diff --git a/.gitattributes b/.gitattributes index 3bef1a9dd..caf72565f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,7 @@ -# Shell scripts must keep LF endings — a CRLF shebang breaks execution on HPC/Linux. -*.sh eol=lf +# All files use Unix line endings (LF) — see AGENTS.md. text=auto lets git +# detect binaries; eol=lf normalises every text file on checkout and checkin, +# subsuming the earlier targeted shebang/script rules. Tree-wide CRLF +# normalisation landed alongside this file (2026-07-25). +* text=auto eol=lf +# Archived historical PR patches must stay byte-exact for git apply. +patches/** -text diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 223ffda10..a03cb3341 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,17 +1,17 @@ -version: 2 - -build: - os: ubuntu-22.04 - tools: - python: "3.12" - -python: - install: - - method: pip - path: . - extra_requirements: - - docs - -sphinx: - configuration: docs/conf.py +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +python: + install: + - method: pip + path: . + extra_requirements: + - docs + +sphinx: + configuration: docs/conf.py fail_on_warning: false \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 077ffad8d..4ef5999aa 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,307 +1,307 @@ -# PyAutoLens Code of Conduct - -**Table of Contents** - -- [The Short Version](#the-short-version) -- [The Longer Version](#the-longer-version) - - [PyAutoLens Diversity Statement](#project-diversity-statement) - - [PyAutoLens Code of Conduct: Introduction & Scope](#project-code-of-conduct-introduction--scope) - - [Standards for Behavior](#standards-for-behavior) - - [Unacceptable Behavior](#unacceptable-behavior) - - [Reporting Guidelines](#reporting-guidelines) - - [How to Submit a Report](#how-to-submit-a-report) - - [Person(s) Responsible for Resolving Complaints](#persons-responsible-for-resolving-complaints) - - [Conflicts of Interest](#conflicts-of-interest) - - [What to Include in a Report](#what-to-include-in-a-report) - - [Enforcement: What Happens After a Report is Filed?](#enforcement-what-happens-after-a-report-is-filed) - - [Acknowledgment and Responding to Immediate Needs](#acknowledgment-and-responding-to-immediate-needs) - - [Reviewing the Report](#reviewing-the-report) - - [Contacting the Person Reported](#contacting-the-person-reported) - - [Response and Potential Consequences](#response-and-potential-consequences) - - [Appealing a Decision](#appealing-a-decision) - - [Timeline Summary:](#timeline-summary) - - [Confirming Receipt](#confirming-receipt) - - [Reviewing the Report](#reviewing-the-report-1) - - [Consequences & Resolution](#consequences--resolution) -- [License](#license) - -## The Short Version - -Be kind to others. Do not insult or put down others. Behave professionally. Remember that harassment and sexist, -racist, or exclusionary jokes are not appropriate for PyAutoLens. - -All communication should be appropriate for a professional audience including people of many different backgrounds. -Sexual language and imagery is not appropriate. - -PyAutoLens is dedicated to providing a harassment-free community for everyone, regardless of gender, sexual orientation, -gender identity and expression, disability, physical appearance, body size, race, or religion. We do not tolerate -harassment of community members in any form. - -Thank you for helping make this a welcoming, friendly community for all. - -## The Longer Version - -### PyAutoLens Diversity Statement - -PyAutoLens welcomes and encourages participation in our community by people of all backgrounds and identities. We -are committed to promoting and sustaining a culture that values mutual respect, tolerance, and learning, and we work -together as a community to help each other live out these values. - -We have created this diversity statement because we believe that a diverse community is stronger, more vibrant, -and produces better software and better science. A diverse community where people treat each other with respect has -more potential contributors, more sources for ideas, and fewer shared assumptions that might hinder development -or research. - -Although we have phrased the formal diversity statement generically to make it all-inclusive, we recognize that there -are specific identities that are impacted by systemic discrimination and marginalization. We welcome all people to -participate in the PyAutoLens community regardless of their identity or background. - -### PyAutoLens Code of Conduct: Introduction & Scope - -This code of conduct should be honored by everyone who participates in the PyAutoLens community. It should be -honored in any PyAutoLens-related activities, by anyone claiming affiliation with PyAutoLens, and especially when -someone is representing PyAutoLens in any role (including as an event volunteer or speaker). - -This code of conduct applies to all spaces managed by PyAutoLens, including all public and private mailing lists, -issue trackers, wikis, forums, and any other communication channel used by our community. The code of conduct equally -applies at PyAutoLens events and governs standards of behavior for attendees, speakers, volunteers, booth staff, -and event sponsors. - -This code is not exhaustive or complete. It serves to distill our understanding of a collaborative, inclusive -community culture. Please try to follow this code in spirit as much as in letter, to create a friendly and -productive environment that enriches the PyAutoLens community. - -The PyAutoLens Code of Conduct follows below. - -### Standards for Behavior - -PyAutoLens is a worldwide community. All communication should be appropriate for a professional audience including -people of many different backgrounds. - -**Please always be kind and courteous. There's never a need to be mean or rude or disrespectful.** Thank you for -helping make this a welcoming, friendly community for all. - -We strive to: - -**Be empathetic, welcoming, friendly, and patient.** We remember that PyAutoLens is crafted by human beings who -deserve to be treated with kindness and empathy. We work together to resolve conflict and assume good intentions. -We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal -attack. A community where people feel uncomfortable or threatened is not a productive one. - -**Be collaborative.** Our work depends on the participation of many people, and in turn others depend on our work. -Open source communities depend on effective and friendly collaboration to achieve their goals. - -**Be inquisitive.** Nobody knows everything! Asking questions early avoids many problems later, so we encourage -questions, although we may direct them to the appropriate forum. We will try hard to be responsive and helpful. - -**Be careful in the words that we choose.** We are careful and respectful in our communication and we take -responsibility for our own speech. Be kind to others. Do not insult or put down other members of the community. - -#### Unacceptable Behavior - -We are committed to making participation in this community a harassment-free experience. - -We will not accept harassment or other exclusionary behaviours, such as: - -- The use of sexualized language or imagery -- Excessive profanity (please avoid curse words; people differ greatly in their sensitivity to swearing) -- Posting sexually explicit or violent material -- Violent or intimidating threats or language directed against another person -- Inappropriate physical contact and/or unwelcome sexual attention or sexual comments -- Sexist, racist, or otherwise discriminatory jokes and language -- Trolling or insulting and derogatory comments -- Written or verbal comments which have the effect of excluding people on the basis of membership in a specific group, -including level of experience, gender, gender identity and expression, sexual orientation, disability, neurotype, -personal appearance, body size, race, ethnicity, age, religion, or nationality -- Public or private harassment -- Sharing private content, such as emails sent privately or non-publicly, or direct message history, without the -sender's consent -- Continuing to initiate interaction (such as photography, recording, messaging, or conversation) with someone after -being asked to stop -- Sustained disruption of talks, events, or communications, such as heckling of a speaker -- Publishing (or threatening to post) other people's personally identifying information ("doxing"), such as -physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct -- Advocating for, or encouraging, any of the above behaviors - -### Reporting Guidelines - -If you believe someone is violating the code of conduct, please report this in a timely manner. Code of conduct -violations reduce the value of the community for everyone. The PyAutoLens leadership team takes reports of misconduct -very seriously and is committed to preserving and maintaining the welcoming nature of our community. - -**All reports will be kept confidential.** - -In some cases we may determine that a public statement will need to be made. If that's the case, the identities of -all involved parties and reporters will remain confidential unless those individuals instruct us otherwise. - -All complaints will be reviewed and investigated and will result in a response that is deemed necessary and -appropriate to the circumstances. The PyAutoLens team commits to maintaining confidentiality with regard to the -reporter of an incident. - -For possibly unintentional breaches of the code of conduct, you may want to respond to the person and point out -this code of conduct (either in public or in private, whatever is most appropriate). If you would prefer not to do -that, please report the issue to PyAutoLens directly, or ask James Nightingale for advice in confidence. Complete contact -information is below, under "How to Submit a Report." - -Take care of each other. Alert PyAutoLens if you notice a dangerous situation, someone in distress, or violations of -this code of conduct, even if they seem inconsequential. - -#### How to Submit a Report - -**If you feel your safety is in jeopardy or the situation is an emergency, we urge you to contact local law enforcement before making a report to PyAutoLens.** (In the U.K., dial 999.) - -PyAutoLens is committed to promptly addressing any reported issues. If you have experienced or witnessed behavior that -violates the PyAutoLens Code of Conduct, please report it by sending an email to one of the members of the PyAutoLens -CoC Enforcement Team. - -#### Person(s) Responsible for Resolving Complaints - -All reports of breaches of the code of conduct will be investigated and handled by the **PyAutoLens Code of Conduct Enforcement Team**. - -The current PyAutoLens Code of Conduct Enforcement Team consists of: - -- James Nightingale - - - [*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) - -#### Conflicts of Interest - -In the event of any conflict of interest, the team member will immediately notify the PyAutoLens Code of Conduct -Enforcement Team and recurse themselves if necessary. - -#### What to Include in a Report - -Our ability to address any code of conduct breaches in a timely and effective manner is impacted by the amount of -information you can provide, so, **our reporting form asks you to include as much of the following information as you can**: - -- **Your contact info** (so we can get in touch with you if we need to follow up). This will be kept confidential. -If you wish to remain anonymous, your information will not be shared beyond the person receiving the initial report. -- The **approximate time and location of the incident** (please be as specific as possible) -- **Identifying information** (e.g. name, nickname, screen name, physical description) of the individual whose -behavior is being reported -- **Description of the behavior** (if reporting harassing language, please be specific about the words -used), **your account of what happened**, and any available **supporting records** (e.g. email, GitHub issue, screenshots, etc.) -- **Description of the circumstances/context** surrounding the incident -- Let us know **if the incident is ongoing**, and/or if this is part of an ongoing pattern of behavior -- Names and contact info, if possible, of **anyone else who witnessed** or was involved in this incident. (Did -anyone else observe the incident?) -- **Any other relevant information** you believe we should have - -At PyAutoLens Events: Event staff will attempt to gather and write down the above information from anyone making a -verbal report in-person at an event. Recording the details in writing is exceedingly important in order for us to -effectively respond to reports. If event staff write down a report taken verbally, then the person making the -report will be asked to review the written report for accuracy. - -**If urgent action is needed regarding an incident at an in-person event, we strongly encourage you to reach out to the local event staff for immediate assistance.** - -### Enforcement: What Happens After a Report is Filed? - -What happens after a report is filed? - -#### Acknowledgment and Responding to Immediate Needs - -PyAutoLens and/or our event staff will attempt to ensure your safety and help with any immediate needs, particularly -at an in-person event. PyAutoLens will make every effort to **acknowledge receipt within 24 hours** (and we'll aim -for much more quickly than that). - - - -#### Reviewing the Report - -PyAutoLens will make all efforts to **review the incident within three days** and determine: - -- Whether this is an ongoing situation, or if there is a threat to anyone's physical safety -- What happened -- Whether this event constitutes a code of conduct violation -- Who the bad actor was, if any - -#### Contacting the Person Reported - -After PyAutoLens has had time to review and discuss the report, someone will attempt to contact the person who is the -subject of the report to inform them of what has been reported about them. We will then ask that person for their -account of what happened. - -#### Response and Potential Consequences - -Once PyAutoLens has completed our investigation of the report, we will make a decision as to how to respond. The -person making a report will not normally be consulted as to the proposed resolution of the issue, except insofar as -we need to understand how to help them feel safe. - -Potential consequences for violating the PyAutoLens code of conduct include: - -- Nothing (if we determine that no violation occurred) -- Private feedback or reprimand from PyAutoLens to the individual(s) involved -- Warning the person to cease their behavior and that any further reports will result in sanctions -- A public announcement that an incident occurred -- Mediation (only if both reporter and reportee agree) -- An imposed vacation (e.g. asking someone to "take a week off" from a mailing list) -- A permanent or temporary ban from some or all PyAutoLens spaces (mailing lists, GitHub repos, in-person events, etc.) -- Assistance to the complainant with a report to other bodies, for example, institutional offices or appropriate law enforcement agencies -- Removing a person from PyAutoLens membership or other formal affiliation -- Publishing an account of the harassment and calling for the resignation of the alleged harasser from their -responsibilities (usually pursued by people without formal authority: may be called for if the person is the -event leader, or refuses to stand aside from the conflict of interest, or similar) -- Any other response that PyAutoLens deems necessary and appropriate to the situation - -At PyAutoLens events, if a participant engages in behavior that violates this code of conduct, the conference -organizers and staff may take any action they deem appropriate. - -Potential consequences for violating the PyAutoLens Code of Conduct at an in-person event include: - -- Warning the person to cease their behavior and that any further reports will result in sanctions -- Requiring that the person avoid any interaction with, and physical proximity to, the person they are harassing -for the remainder of the event -- Ending a talk that violates the policy early -- Not publishing the video or slides of a talk that violated the policy -- Not allowing a speaker who violated the policy to give (further) talks at the event now or in the future -- Immediately ending any event volunteer responsibilities and privileges the reported person holds -- Requiring that the person not volunteer for future events PyAutoLens runs (either indefinitely or for a certain time period) -- Expelling the person from the event without a refund -- Requiring that the person immediately leave the event and not return -- Banning the person from future events (either indefinitely or for a certain time period) -- Any other response that PyAutoLens deems necessary and appropriate to the situation - -No one espousing views or values contrary to the standards of our code of conduct will be permitted to hold any -position representing PyAutoLens, including volunteer positions. PyAutoLens has the right and responsibility to -remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not -aligned with this code of conduct. - -We aim to **respond within one week** to the original reporter with either a resolution or an explanation of why the -situation is not yet resolved. - -We will contact the person who is the subject of the report to let them know what actions will be taken as a result of -the report, if any. - -Our policy is to make sure that everyone aware of the initial incident is also made aware that official action has -been taken, while still respecting the privacy of individuals. PyAutoLens may choose to make a public report of the -incident, while maintaining the anonymity of those involved. - -#### Appealing a Decision - -To appeal a decision of PyAutoLens, contact James Nightingale via email at -[*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) with your appeal and -the Leadership Team will review the case. - -### Timeline Summary: - -#### Confirming Receipt - -PyAutoLens will make every effort to acknowledge receipt of a report **within 24 hours** (and we'll aim for much more -quickly than that). - -#### Reviewing the Report - -PyAutoLens will make all efforts to review the incident **within three days**. - -#### Consequences & Resolution - -We aim to respond **within one week** to the original reporter with either a resolution or an explanation of why -the situation is not yet resolved. - -## License - -This code of conduct has been adapted from [*NUMFOCUS code of conduct*](https://numfocus.org/code-of-conduct), -which is adapted from numerous sources, including the [*Geek Feminism wiki, created by the Ada Initiative and other volunteers, which is under a Creative Commons Zero license*](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy), the [*Contributor Covenant version 1.2.0*](http://contributor-covenant.org/version/1/2/0/), the [*Bokeh Code of Conduct*](https://github.com/bokeh/bokeh/blob/main/docs/CODE_OF_CONDUCT.md), the [*SciPy Code of Conduct*](https://github.com/jupyter/governance/blob/main/conduct/enforcement.md), the [*Carpentries Code of Conduct*](https://docs.carpentries.org/topic_folders/policies/code-of-conduct.html#enforcement-manual), and the [*NeurIPS Code of Conduct*](https://neurips.cc/public/CodeOfConduct). - +# PyAutoLens Code of Conduct + +**Table of Contents** + +- [The Short Version](#the-short-version) +- [The Longer Version](#the-longer-version) + - [PyAutoLens Diversity Statement](#project-diversity-statement) + - [PyAutoLens Code of Conduct: Introduction & Scope](#project-code-of-conduct-introduction--scope) + - [Standards for Behavior](#standards-for-behavior) + - [Unacceptable Behavior](#unacceptable-behavior) + - [Reporting Guidelines](#reporting-guidelines) + - [How to Submit a Report](#how-to-submit-a-report) + - [Person(s) Responsible for Resolving Complaints](#persons-responsible-for-resolving-complaints) + - [Conflicts of Interest](#conflicts-of-interest) + - [What to Include in a Report](#what-to-include-in-a-report) + - [Enforcement: What Happens After a Report is Filed?](#enforcement-what-happens-after-a-report-is-filed) + - [Acknowledgment and Responding to Immediate Needs](#acknowledgment-and-responding-to-immediate-needs) + - [Reviewing the Report](#reviewing-the-report) + - [Contacting the Person Reported](#contacting-the-person-reported) + - [Response and Potential Consequences](#response-and-potential-consequences) + - [Appealing a Decision](#appealing-a-decision) + - [Timeline Summary:](#timeline-summary) + - [Confirming Receipt](#confirming-receipt) + - [Reviewing the Report](#reviewing-the-report-1) + - [Consequences & Resolution](#consequences--resolution) +- [License](#license) + +## The Short Version + +Be kind to others. Do not insult or put down others. Behave professionally. Remember that harassment and sexist, +racist, or exclusionary jokes are not appropriate for PyAutoLens. + +All communication should be appropriate for a professional audience including people of many different backgrounds. +Sexual language and imagery is not appropriate. + +PyAutoLens is dedicated to providing a harassment-free community for everyone, regardless of gender, sexual orientation, +gender identity and expression, disability, physical appearance, body size, race, or religion. We do not tolerate +harassment of community members in any form. + +Thank you for helping make this a welcoming, friendly community for all. + +## The Longer Version + +### PyAutoLens Diversity Statement + +PyAutoLens welcomes and encourages participation in our community by people of all backgrounds and identities. We +are committed to promoting and sustaining a culture that values mutual respect, tolerance, and learning, and we work +together as a community to help each other live out these values. + +We have created this diversity statement because we believe that a diverse community is stronger, more vibrant, +and produces better software and better science. A diverse community where people treat each other with respect has +more potential contributors, more sources for ideas, and fewer shared assumptions that might hinder development +or research. + +Although we have phrased the formal diversity statement generically to make it all-inclusive, we recognize that there +are specific identities that are impacted by systemic discrimination and marginalization. We welcome all people to +participate in the PyAutoLens community regardless of their identity or background. + +### PyAutoLens Code of Conduct: Introduction & Scope + +This code of conduct should be honored by everyone who participates in the PyAutoLens community. It should be +honored in any PyAutoLens-related activities, by anyone claiming affiliation with PyAutoLens, and especially when +someone is representing PyAutoLens in any role (including as an event volunteer or speaker). + +This code of conduct applies to all spaces managed by PyAutoLens, including all public and private mailing lists, +issue trackers, wikis, forums, and any other communication channel used by our community. The code of conduct equally +applies at PyAutoLens events and governs standards of behavior for attendees, speakers, volunteers, booth staff, +and event sponsors. + +This code is not exhaustive or complete. It serves to distill our understanding of a collaborative, inclusive +community culture. Please try to follow this code in spirit as much as in letter, to create a friendly and +productive environment that enriches the PyAutoLens community. + +The PyAutoLens Code of Conduct follows below. + +### Standards for Behavior + +PyAutoLens is a worldwide community. All communication should be appropriate for a professional audience including +people of many different backgrounds. + +**Please always be kind and courteous. There's never a need to be mean or rude or disrespectful.** Thank you for +helping make this a welcoming, friendly community for all. + +We strive to: + +**Be empathetic, welcoming, friendly, and patient.** We remember that PyAutoLens is crafted by human beings who +deserve to be treated with kindness and empathy. We work together to resolve conflict and assume good intentions. +We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal +attack. A community where people feel uncomfortable or threatened is not a productive one. + +**Be collaborative.** Our work depends on the participation of many people, and in turn others depend on our work. +Open source communities depend on effective and friendly collaboration to achieve their goals. + +**Be inquisitive.** Nobody knows everything! Asking questions early avoids many problems later, so we encourage +questions, although we may direct them to the appropriate forum. We will try hard to be responsive and helpful. + +**Be careful in the words that we choose.** We are careful and respectful in our communication and we take +responsibility for our own speech. Be kind to others. Do not insult or put down other members of the community. + +#### Unacceptable Behavior + +We are committed to making participation in this community a harassment-free experience. + +We will not accept harassment or other exclusionary behaviours, such as: + +- The use of sexualized language or imagery +- Excessive profanity (please avoid curse words; people differ greatly in their sensitivity to swearing) +- Posting sexually explicit or violent material +- Violent or intimidating threats or language directed against another person +- Inappropriate physical contact and/or unwelcome sexual attention or sexual comments +- Sexist, racist, or otherwise discriminatory jokes and language +- Trolling or insulting and derogatory comments +- Written or verbal comments which have the effect of excluding people on the basis of membership in a specific group, +including level of experience, gender, gender identity and expression, sexual orientation, disability, neurotype, +personal appearance, body size, race, ethnicity, age, religion, or nationality +- Public or private harassment +- Sharing private content, such as emails sent privately or non-publicly, or direct message history, without the +sender's consent +- Continuing to initiate interaction (such as photography, recording, messaging, or conversation) with someone after +being asked to stop +- Sustained disruption of talks, events, or communications, such as heckling of a speaker +- Publishing (or threatening to post) other people's personally identifying information ("doxing"), such as +physical or electronic addresses, without explicit permission +- Other unethical or unprofessional conduct +- Advocating for, or encouraging, any of the above behaviors + +### Reporting Guidelines + +If you believe someone is violating the code of conduct, please report this in a timely manner. Code of conduct +violations reduce the value of the community for everyone. The PyAutoLens leadership team takes reports of misconduct +very seriously and is committed to preserving and maintaining the welcoming nature of our community. + +**All reports will be kept confidential.** + +In some cases we may determine that a public statement will need to be made. If that's the case, the identities of +all involved parties and reporters will remain confidential unless those individuals instruct us otherwise. + +All complaints will be reviewed and investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. The PyAutoLens team commits to maintaining confidentiality with regard to the +reporter of an incident. + +For possibly unintentional breaches of the code of conduct, you may want to respond to the person and point out +this code of conduct (either in public or in private, whatever is most appropriate). If you would prefer not to do +that, please report the issue to PyAutoLens directly, or ask James Nightingale for advice in confidence. Complete contact +information is below, under "How to Submit a Report." + +Take care of each other. Alert PyAutoLens if you notice a dangerous situation, someone in distress, or violations of +this code of conduct, even if they seem inconsequential. + +#### How to Submit a Report + +**If you feel your safety is in jeopardy or the situation is an emergency, we urge you to contact local law enforcement before making a report to PyAutoLens.** (In the U.K., dial 999.) + +PyAutoLens is committed to promptly addressing any reported issues. If you have experienced or witnessed behavior that +violates the PyAutoLens Code of Conduct, please report it by sending an email to one of the members of the PyAutoLens +CoC Enforcement Team. + +#### Person(s) Responsible for Resolving Complaints + +All reports of breaches of the code of conduct will be investigated and handled by the **PyAutoLens Code of Conduct Enforcement Team**. + +The current PyAutoLens Code of Conduct Enforcement Team consists of: + +- James Nightingale + + - [*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) + +#### Conflicts of Interest + +In the event of any conflict of interest, the team member will immediately notify the PyAutoLens Code of Conduct +Enforcement Team and recurse themselves if necessary. + +#### What to Include in a Report + +Our ability to address any code of conduct breaches in a timely and effective manner is impacted by the amount of +information you can provide, so, **our reporting form asks you to include as much of the following information as you can**: + +- **Your contact info** (so we can get in touch with you if we need to follow up). This will be kept confidential. +If you wish to remain anonymous, your information will not be shared beyond the person receiving the initial report. +- The **approximate time and location of the incident** (please be as specific as possible) +- **Identifying information** (e.g. name, nickname, screen name, physical description) of the individual whose +behavior is being reported +- **Description of the behavior** (if reporting harassing language, please be specific about the words +used), **your account of what happened**, and any available **supporting records** (e.g. email, GitHub issue, screenshots, etc.) +- **Description of the circumstances/context** surrounding the incident +- Let us know **if the incident is ongoing**, and/or if this is part of an ongoing pattern of behavior +- Names and contact info, if possible, of **anyone else who witnessed** or was involved in this incident. (Did +anyone else observe the incident?) +- **Any other relevant information** you believe we should have + +At PyAutoLens Events: Event staff will attempt to gather and write down the above information from anyone making a +verbal report in-person at an event. Recording the details in writing is exceedingly important in order for us to +effectively respond to reports. If event staff write down a report taken verbally, then the person making the +report will be asked to review the written report for accuracy. + +**If urgent action is needed regarding an incident at an in-person event, we strongly encourage you to reach out to the local event staff for immediate assistance.** + +### Enforcement: What Happens After a Report is Filed? + +What happens after a report is filed? + +#### Acknowledgment and Responding to Immediate Needs + +PyAutoLens and/or our event staff will attempt to ensure your safety and help with any immediate needs, particularly +at an in-person event. PyAutoLens will make every effort to **acknowledge receipt within 24 hours** (and we'll aim +for much more quickly than that). + + + +#### Reviewing the Report + +PyAutoLens will make all efforts to **review the incident within three days** and determine: + +- Whether this is an ongoing situation, or if there is a threat to anyone's physical safety +- What happened +- Whether this event constitutes a code of conduct violation +- Who the bad actor was, if any + +#### Contacting the Person Reported + +After PyAutoLens has had time to review and discuss the report, someone will attempt to contact the person who is the +subject of the report to inform them of what has been reported about them. We will then ask that person for their +account of what happened. + +#### Response and Potential Consequences + +Once PyAutoLens has completed our investigation of the report, we will make a decision as to how to respond. The +person making a report will not normally be consulted as to the proposed resolution of the issue, except insofar as +we need to understand how to help them feel safe. + +Potential consequences for violating the PyAutoLens code of conduct include: + +- Nothing (if we determine that no violation occurred) +- Private feedback or reprimand from PyAutoLens to the individual(s) involved +- Warning the person to cease their behavior and that any further reports will result in sanctions +- A public announcement that an incident occurred +- Mediation (only if both reporter and reportee agree) +- An imposed vacation (e.g. asking someone to "take a week off" from a mailing list) +- A permanent or temporary ban from some or all PyAutoLens spaces (mailing lists, GitHub repos, in-person events, etc.) +- Assistance to the complainant with a report to other bodies, for example, institutional offices or appropriate law enforcement agencies +- Removing a person from PyAutoLens membership or other formal affiliation +- Publishing an account of the harassment and calling for the resignation of the alleged harasser from their +responsibilities (usually pursued by people without formal authority: may be called for if the person is the +event leader, or refuses to stand aside from the conflict of interest, or similar) +- Any other response that PyAutoLens deems necessary and appropriate to the situation + +At PyAutoLens events, if a participant engages in behavior that violates this code of conduct, the conference +organizers and staff may take any action they deem appropriate. + +Potential consequences for violating the PyAutoLens Code of Conduct at an in-person event include: + +- Warning the person to cease their behavior and that any further reports will result in sanctions +- Requiring that the person avoid any interaction with, and physical proximity to, the person they are harassing +for the remainder of the event +- Ending a talk that violates the policy early +- Not publishing the video or slides of a talk that violated the policy +- Not allowing a speaker who violated the policy to give (further) talks at the event now or in the future +- Immediately ending any event volunteer responsibilities and privileges the reported person holds +- Requiring that the person not volunteer for future events PyAutoLens runs (either indefinitely or for a certain time period) +- Expelling the person from the event without a refund +- Requiring that the person immediately leave the event and not return +- Banning the person from future events (either indefinitely or for a certain time period) +- Any other response that PyAutoLens deems necessary and appropriate to the situation + +No one espousing views or values contrary to the standards of our code of conduct will be permitted to hold any +position representing PyAutoLens, including volunteer positions. PyAutoLens has the right and responsibility to +remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not +aligned with this code of conduct. + +We aim to **respond within one week** to the original reporter with either a resolution or an explanation of why the +situation is not yet resolved. + +We will contact the person who is the subject of the report to let them know what actions will be taken as a result of +the report, if any. + +Our policy is to make sure that everyone aware of the initial incident is also made aware that official action has +been taken, while still respecting the privacy of individuals. PyAutoLens may choose to make a public report of the +incident, while maintaining the anonymity of those involved. + +#### Appealing a Decision + +To appeal a decision of PyAutoLens, contact James Nightingale via email at +[*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) with your appeal and +the Leadership Team will review the case. + +### Timeline Summary: + +#### Confirming Receipt + +PyAutoLens will make every effort to acknowledge receipt of a report **within 24 hours** (and we'll aim for much more +quickly than that). + +#### Reviewing the Report + +PyAutoLens will make all efforts to review the incident **within three days**. + +#### Consequences & Resolution + +We aim to respond **within one week** to the original reporter with either a resolution or an explanation of why +the situation is not yet resolved. + +## License + +This code of conduct has been adapted from [*NUMFOCUS code of conduct*](https://numfocus.org/code-of-conduct), +which is adapted from numerous sources, including the [*Geek Feminism wiki, created by the Ada Initiative and other volunteers, which is under a Creative Commons Zero license*](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy), the [*Contributor Covenant version 1.2.0*](http://contributor-covenant.org/version/1/2/0/), the [*Bokeh Code of Conduct*](https://github.com/bokeh/bokeh/blob/main/docs/CODE_OF_CONDUCT.md), the [*SciPy Code of Conduct*](https://github.com/jupyter/governance/blob/main/conduct/enforcement.md), the [*Carpentries Code of Conduct*](https://docs.carpentries.org/topic_folders/policies/code-of-conduct.html#enforcement-manual), and the [*NeurIPS Code of Conduct*](https://neurips.cc/public/CodeOfConduct). + **PyAutoLens Code of Conduct is licensed under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/).** \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1540e2bb9..4ab0f621b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,144 +1,144 @@ -# AI-Assisted Development - -This project uses an AI-first development workflow. Most features, bug fixes, and improvements are implemented through AI coding agents (Claude Code, GitHub Copilot, OpenAI Codex) working from structured issue descriptions. - -## How It Works - -1. **Issues are the starting point** — Every task begins as a GitHub issue with a structured format: an overview, a human-readable plan, a detailed implementation plan (in a collapsible block), and optionally the original prompt that generated the issue. - -2. **AI agents pick up issues** — Issues can be assigned to AI coding agents (e.g. GitHub Copilot) which read the issue description, `AGENTS.md`, and `CLAUDE.md` for context, then implement the changes autonomously. - -3. **Human review** — All AI-generated pull requests are reviewed by maintainers before merging. - -## Maintainer Workflow - -Maintainer-driven dev work starts as a prompt file in -**[PyAutoPrompt](https://github.com/PyAutoLabs/PyAutoPrompt)** — the public -workflow repo that hosts the PyAuto task registry and the prompt-coupled -Claude Code skills. The pipeline: - -1. Write the task as `PyAutoPrompt//.md` (free-form markdown - describing what to do, with `@RepoName/path/to/file.py` references). -2. `/start_dev /.md` — reads the prompt, audits the code, - drafts the GitHub issue you see in this repo, and files it. -3. `/start_library` or `/start_workspace` — opens a feature worktree under - `~/Code/PyAutoLabs-wt//`. -4. `/ship_library` / `/ship_workspace` — runs tests, opens the PR, and - tracks state in `PyAutoPrompt/active.md`. - -External contributors don't need PyAutoPrompt access — open an issue using -the templates in this repo and the same machinery handles it on our end. - -## Creating an Issue - -When opening an issue, please use the provided issue templates. The **Feature / Task Request** template follows our standard format: - -- **Overview** — What and why, in 2-4 sentences -- **Plan** — High-level bullet points (human-readable) -- **Detailed implementation plan** — File paths, steps, key files (in a collapsible block) -- **Original Prompt** — If you used an AI to help draft the issue, include the original prompt - -If your feature involves a specific calculation, algorithm, or small piece of functionality — **include example code**. Even a rough script, a working prototype, or a snippet showing the existing behaviour you want to change makes a huge difference. Code examples give AI agents and human contributors concrete context to work from, and dramatically reduce misunderstandings about what you're asking for. - -This structure ensures that both human contributors and AI agents can understand and act on the issue effectively. - -## Contributing Without AI - -Traditional contributions are equally welcome! If you prefer to work without AI tools, simply follow the development setup and pull request guidelines below. The issue templates are helpful for any contributor, AI or human. - ---- - -## Community Guidelines - -We strive to maintain a welcoming, respectful, and inclusive community. All contributors—whether opening issues, submitting pull requests, reviewing code, or participating in discussions—are expected to follow these guidelines. - -- Be respectful and considerate of others. -- Assume good intent and be patient, especially with newcomers. -- Keep feedback constructive and focused on the work, not the person. -- Communicate clearly and professionally. -- Respect maintainers’ time and decisions. - -Harassment, discrimination, or abusive behavior of any kind will not be tolerated. - -This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you agree to uphold it. - -### Reporting concerns -If you experience or witness behavior that violates these guidelines or the Code of Conduct, please contact the project maintainers privately. - - -# Contributing - -Contributions are welcome and greatly appreciated! - -## Types of Contributions - -### Report Bugs - -Report bugs at https://github.com/PyAutoLabs/PyAutoLens/issues - -If you are playing with the PyAutoLens library and find a bug, please -reporting it including: - -* Your operating system name and version. -* Any details about your Python environment. -* Detailed steps to reproduce the bug. - -### Propose New Features - -The best way to send feedback is to open an issue at -https://github.com/PyAutoLabs/PyAutoLens -with tag *enhancement*. - -If you are proposing a nnew feature: - -* Explain in detail how it should work. -* Keep the scope as narrow as possible, to make it easier to implement. - -### Implement Features -Look through the Git issues for operator or feature requests. -Anything tagged with *enhancement* is open to whoever wants to -implement it. - -### Add Examples or improve Documentation -Writing new features is not the only way to get involved and -contribute. Create examples with existing features as well -as improving the documentation of existing operators is as important -as making new non-linear searches and very much encouraged. - - -## Getting Started to contribute - -Ready to contribute? - -1. Follow the installation instructions for installing **PyAutoLens** (and parent projects) from source root -on our [readthedocs](https://pyautolens.readthedocs.io/en/latest/installation/source.html). - -2. Create a feature branch for local development (for **PyAutoLens** and every parent project where changes are implemented): - ``` - git checkout -b feature/name-of-your-branch - ``` - Now you can make your changes locally. - -3. When you're done making changes, check that old and new tests pass successfully: - ``` - cd PyAutoLens/test_autolens - python3 -m pytest - ``` - -4. Commit your changes and push your branch to GitHub:: - ``` - git add . - git commit -m "Your detailed description of your changes." - git push origin feature/name-of-your-branch - ``` - Remember to add ``-u`` when pushing the branch for the first time. - -5. Submit a pull request through the GitHub website. - - -### Pull Request Guidelines - -Before you submit a pull request, check that it meets these guidelines: - -1. The pull request should include new tests for all the core routines that have been developed. -2. If the pull request adds functionality, the docs should be updated accordingly. +# AI-Assisted Development + +This project uses an AI-first development workflow. Most features, bug fixes, and improvements are implemented through AI coding agents (Claude Code, GitHub Copilot, OpenAI Codex) working from structured issue descriptions. + +## How It Works + +1. **Issues are the starting point** — Every task begins as a GitHub issue with a structured format: an overview, a human-readable plan, a detailed implementation plan (in a collapsible block), and optionally the original prompt that generated the issue. + +2. **AI agents pick up issues** — Issues can be assigned to AI coding agents (e.g. GitHub Copilot) which read the issue description, `AGENTS.md`, and `CLAUDE.md` for context, then implement the changes autonomously. + +3. **Human review** — All AI-generated pull requests are reviewed by maintainers before merging. + +## Maintainer Workflow + +Maintainer-driven dev work starts as a prompt file in +**[PyAutoPrompt](https://github.com/PyAutoLabs/PyAutoPrompt)** — the public +workflow repo that hosts the PyAuto task registry and the prompt-coupled +Claude Code skills. The pipeline: + +1. Write the task as `PyAutoPrompt//.md` (free-form markdown + describing what to do, with `@RepoName/path/to/file.py` references). +2. `/start_dev /.md` — reads the prompt, audits the code, + drafts the GitHub issue you see in this repo, and files it. +3. `/start_library` or `/start_workspace` — opens a feature worktree under + `~/Code/PyAutoLabs-wt//`. +4. `/ship_library` / `/ship_workspace` — runs tests, opens the PR, and + tracks state in `PyAutoPrompt/active.md`. + +External contributors don't need PyAutoPrompt access — open an issue using +the templates in this repo and the same machinery handles it on our end. + +## Creating an Issue + +When opening an issue, please use the provided issue templates. The **Feature / Task Request** template follows our standard format: + +- **Overview** — What and why, in 2-4 sentences +- **Plan** — High-level bullet points (human-readable) +- **Detailed implementation plan** — File paths, steps, key files (in a collapsible block) +- **Original Prompt** — If you used an AI to help draft the issue, include the original prompt + +If your feature involves a specific calculation, algorithm, or small piece of functionality — **include example code**. Even a rough script, a working prototype, or a snippet showing the existing behaviour you want to change makes a huge difference. Code examples give AI agents and human contributors concrete context to work from, and dramatically reduce misunderstandings about what you're asking for. + +This structure ensures that both human contributors and AI agents can understand and act on the issue effectively. + +## Contributing Without AI + +Traditional contributions are equally welcome! If you prefer to work without AI tools, simply follow the development setup and pull request guidelines below. The issue templates are helpful for any contributor, AI or human. + +--- + +## Community Guidelines + +We strive to maintain a welcoming, respectful, and inclusive community. All contributors—whether opening issues, submitting pull requests, reviewing code, or participating in discussions—are expected to follow these guidelines. + +- Be respectful and considerate of others. +- Assume good intent and be patient, especially with newcomers. +- Keep feedback constructive and focused on the work, not the person. +- Communicate clearly and professionally. +- Respect maintainers’ time and decisions. + +Harassment, discrimination, or abusive behavior of any kind will not be tolerated. + +This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you agree to uphold it. + +### Reporting concerns +If you experience or witness behavior that violates these guidelines or the Code of Conduct, please contact the project maintainers privately. + + +# Contributing + +Contributions are welcome and greatly appreciated! + +## Types of Contributions + +### Report Bugs + +Report bugs at https://github.com/PyAutoLabs/PyAutoLens/issues + +If you are playing with the PyAutoLens library and find a bug, please +reporting it including: + +* Your operating system name and version. +* Any details about your Python environment. +* Detailed steps to reproduce the bug. + +### Propose New Features + +The best way to send feedback is to open an issue at +https://github.com/PyAutoLabs/PyAutoLens +with tag *enhancement*. + +If you are proposing a nnew feature: + +* Explain in detail how it should work. +* Keep the scope as narrow as possible, to make it easier to implement. + +### Implement Features +Look through the Git issues for operator or feature requests. +Anything tagged with *enhancement* is open to whoever wants to +implement it. + +### Add Examples or improve Documentation +Writing new features is not the only way to get involved and +contribute. Create examples with existing features as well +as improving the documentation of existing operators is as important +as making new non-linear searches and very much encouraged. + + +## Getting Started to contribute + +Ready to contribute? + +1. Follow the installation instructions for installing **PyAutoLens** (and parent projects) from source root +on our [readthedocs](https://pyautolens.readthedocs.io/en/latest/installation/source.html). + +2. Create a feature branch for local development (for **PyAutoLens** and every parent project where changes are implemented): + ``` + git checkout -b feature/name-of-your-branch + ``` + Now you can make your changes locally. + +3. When you're done making changes, check that old and new tests pass successfully: + ``` + cd PyAutoLens/test_autolens + python3 -m pytest + ``` + +4. Commit your changes and push your branch to GitHub:: + ``` + git add . + git commit -m "Your detailed description of your changes." + git push origin feature/name-of-your-branch + ``` + Remember to add ``-u`` when pushing the branch for the first time. + +5. Submit a pull request through the GitHub website. + + +### Pull Request Guidelines + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include new tests for all the core routines that have been developed. +2. If the pull request adds functionality, the docs should be updated accordingly. diff --git a/LICENSE b/LICENSE index 3eb21beb7..7624cb6b6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 Jammy2211 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2018 Jammy2211 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 1f0f44000..187d294c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,22 +1,22 @@ -# MANIFEST.in -exclude .gitignore -include README.md -include setup.cfg -include CITATIONS.md -include LICENSE -include requirements.txt -include optional_requirements.txt - -prune .cache -prune .git -prune build -prune dist - -recursive-exclude *.egg-info * - -recursive-include autolens/config * - -exclude docs - -global-exclude test_autolens +# MANIFEST.in +exclude .gitignore +include README.md +include setup.cfg +include CITATIONS.md +include LICENSE +include requirements.txt +include optional_requirements.txt + +prune .cache +prune .git +prune build +prune dist + +recursive-exclude *.egg-info * + +recursive-include autolens/config * + +exclude docs + +global-exclude test_autolens recursive-exclude test_autolens * \ No newline at end of file diff --git a/autolens/__init__.py b/autolens/__init__.py index eba8d6459..9839c51e4 100644 --- a/autolens/__init__.py +++ b/autolens/__init__.py @@ -1,169 +1,169 @@ -from autonerves import jax_wrapper -from autonerves.dictable import from_dict, from_json, output_to_json, to_dict -from autoarray import preprocess - -from autoarray.dataset.imaging.dataset import Imaging -from autoarray.dataset.interferometer.dataset import ( - Interferometer, -) -from autoarray.dataset.grids import GridsInterface -from autoarray.dataset.dataset_model import DatasetModel -from autoarray.mask.mask_1d import Mask1D -from autoarray.mask.mask_2d import Mask2D -from autoarray.mask.derive.zoom_2d import Zoom2D -from autoarray.operators.over_sampling.over_sampler import OverSampler # noqa -from autoarray.inversion.inversion.dataset_interface import DatasetInterface -from autoarray.inversion.mesh import image_mesh -from autoarray.inversion.mesh import mesh -from autoarray.inversion import regularization as reg -from autoarray.inversion.mesh.image_mesh.abstract import AbstractImageMesh -from autoarray.inversion.mesh.mesh.abstract import AbstractMesh -from autoarray.inversion.regularization.abstract import AbstractRegularization -from autoarray.inversion.pixelization import Pixelization -from autoarray.settings import Settings -from autoarray.inversion.inversion.factory import inversion_from as Inversion -from autoarray.inversion.mappers.abstract import Mapper -from autoarray.inversion.mesh.border_relocator import BorderRelocator -from autoarray.operators.convolver import Convolver -from autoarray.operators.transformer import TransformerDFT -from autoarray.operators.transformer import TransformerNUFFT -from autoarray.operators.transformer import TransformerNUFFTPyNUFFT -from autoarray.structures.arrays.uniform_1d import Array1D -from autoarray.structures.arrays.uniform_2d import Array2D -from autoarray.structures.arrays.rgb import Array2DRGB -from autoarray.structures.arrays.irregular import ArrayIrregular -from autoarray.structures.grids.uniform_1d import Grid1D -from autoarray.structures.grids.uniform_2d import Grid2D -from autoarray.structures.grids.irregular_2d import Grid2DIrregular -from autoarray.inversion.mesh.interpolator.rectangular import ( - InterpolatorRectangular, -) -from autoarray.inversion.mesh.interpolator.delaunay import InterpolatorDelaunay -from autoarray.structures.triangles.shape import Circle -from autoarray.structures.triangles.shape import Triangle -from autoarray.structures.triangles.shape import Square -from autoarray.structures.triangles.shape import Polygon -from autoarray.structures.vectors.uniform import VectorYX2D -from autoarray.structures.vectors.irregular import VectorYX2DIrregular -from autoarray.structures.visibilities import Visibilities -from autoarray.structures.visibilities import VisibilitiesNoiseMap -from autoarray.inversion.mesh.mesh_geometry.rectangular import ( - rectangular_edge_pixel_list_from, -) - -from autogalaxy import cosmology as cosmo -from autogalaxy.analysis.adapt_images.adapt_images import AdaptImages -from autogalaxy.analysis.adapt_images.adapt_images import ( - galaxy_name_image_dict_via_result_from, -) -from autogalaxy.gui.clicker import Clicker -from autogalaxy.gui.scribbler import Scribbler -from autogalaxy.galaxy.galaxy import Galaxy -from autogalaxy.galaxy.galaxies import Galaxies -from autogalaxy.galaxy.galaxy_table import GalaxyTable -from autogalaxy.galaxy.galaxy_table import galaxy_table_from_csv -from autogalaxy.galaxy.galaxy_table import galaxy_table_to_csv -from autogalaxy.galaxy.galaxy_model_csv import GalaxyModelRow -from autogalaxy.galaxy.galaxy_model_csv import GalaxyModelTable -from autogalaxy.galaxy.galaxy_model_csv import galaxy_models_from_csv -from autogalaxy.galaxy.galaxy_model_csv import galaxy_models_to_csv -from autogalaxy.galaxy.galaxy_model_csv import galaxies_from_csv_tables -from autogalaxy.galaxy.galaxy_model_csv import galaxy_af_models_from_csv_tables -from autogalaxy.galaxy.redshift import Redshift - -from autogalaxy.profiles.geometry_profiles import EllProfile -from autogalaxy.profiles import ( - point_sources as ps, - mass as mp, - light_and_mass_profiles as lmp, - light_linear_and_mass_profiles as lmp_linear, - scaling_relations as sr, -) -from autogalaxy.profiles.light.abstract import LightProfile -from autogalaxy.profiles.light import standard as lp -from autogalaxy.profiles.light import snr as lp_snr -from autogalaxy.profiles.light import linear as lp_linear -from autogalaxy.profiles.light import operated as lp_operated -from autogalaxy.profiles import basis as lp_basis -from autogalaxy.profiles.light import ( - linear_operated as lp_linear_operated, -) -from autogalaxy.profiles.light.linear import LightProfileLinearObjFuncList -from autogalaxy.operate.image import OperateImage -from autogalaxy.operate.lens_calc import LensCalc -from autogalaxy import convert - -from . import aggregator as agg -from .analysis import model_util -from .lens import subhalo -from .lens.tracer import Tracer -from .lens.sensitivity import SubhaloSensitivityResult -from .lens.to_inversion import TracerToInversion -from .analysis.positions import PositionsLH -from .imaging.simulator import SimulatorImaging -from .imaging.fit_imaging import FitImaging -from .imaging.model.analysis import AnalysisImaging -from autofit import Latent -from .analysis.latent import LatentLens -from .imaging.model.visualizer import VisualizerImaging -from .interferometer.simulator import SimulatorInterferometer -from .interferometer.fit_interferometer import FitInterferometer -from .interferometer.model.analysis import AnalysisInterferometer -from .interferometer.model.visualizer import VisualizerInterferometer -from .point.dataset import PointDataset -from .point.dataset import list_from_csv -from .point.dataset import output_to_csv -from .point.fit.dataset import FitPointDataset -from .point.fit.fluxes import FitFluxes -from .point.fit.times_delays import FitTimeDelays -from .point.fit.positions.image.abstract import AbstractFitPositionsImagePair -from .point.fit.positions.image.pair import FitPositionsImagePair -from .point.fit.positions.image.pair_all import FitPositionsImagePairAll -from .point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat -from .point.fit.positions.source.separations import FitPositionsSource -from .point.max_separation import SourceMaxSeparation -from .point.model.analysis import AnalysisPoint -from .point.solver import PointSolver -from .point.solver.shape_solver import ShapeSolver -from .weak.dataset import WeakDataset -from .weak.fit import FitWeak -from .weak.model.analysis import AnalysisWeak -from .weak.simulator import SimulatorShearYX - -from . import exc -from . import mock as m -from . import util -from . import potential_correction as pc - -from autonerves import conf -from autonerves.fitsable import ndarray_via_hdu_from -from autonerves.fitsable import ndarray_via_fits_from -from autonerves.fitsable import header_obj_from -from autonerves.fitsable import output_to_fits -from autonerves.fitsable import hdu_list_for_output_from - -conf.instance.register(__file__) - -__version__ = "2026.7.23.1" - -from autonerves import check_version - -check_version(__version__) - - -def __getattr__(name): - if name == "plot": - from . import plot - - globals()["plot"] = plot - return plot - if name == "interop": - import importlib - - interop = importlib.import_module(f"{__name__}.interop") - globals()["interop"] = interop - return interop - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +from autonerves import jax_wrapper +from autonerves.dictable import from_dict, from_json, output_to_json, to_dict +from autoarray import preprocess + +from autoarray.dataset.imaging.dataset import Imaging +from autoarray.dataset.interferometer.dataset import ( + Interferometer, +) +from autoarray.dataset.grids import GridsInterface +from autoarray.dataset.dataset_model import DatasetModel +from autoarray.mask.mask_1d import Mask1D +from autoarray.mask.mask_2d import Mask2D +from autoarray.mask.derive.zoom_2d import Zoom2D +from autoarray.operators.over_sampling.over_sampler import OverSampler # noqa +from autoarray.inversion.inversion.dataset_interface import DatasetInterface +from autoarray.inversion.mesh import image_mesh +from autoarray.inversion.mesh import mesh +from autoarray.inversion import regularization as reg +from autoarray.inversion.mesh.image_mesh.abstract import AbstractImageMesh +from autoarray.inversion.mesh.mesh.abstract import AbstractMesh +from autoarray.inversion.regularization.abstract import AbstractRegularization +from autoarray.inversion.pixelization import Pixelization +from autoarray.settings import Settings +from autoarray.inversion.inversion.factory import inversion_from as Inversion +from autoarray.inversion.mappers.abstract import Mapper +from autoarray.inversion.mesh.border_relocator import BorderRelocator +from autoarray.operators.convolver import Convolver +from autoarray.operators.transformer import TransformerDFT +from autoarray.operators.transformer import TransformerNUFFT +from autoarray.operators.transformer import TransformerNUFFTPyNUFFT +from autoarray.structures.arrays.uniform_1d import Array1D +from autoarray.structures.arrays.uniform_2d import Array2D +from autoarray.structures.arrays.rgb import Array2DRGB +from autoarray.structures.arrays.irregular import ArrayIrregular +from autoarray.structures.grids.uniform_1d import Grid1D +from autoarray.structures.grids.uniform_2d import Grid2D +from autoarray.structures.grids.irregular_2d import Grid2DIrregular +from autoarray.inversion.mesh.interpolator.rectangular import ( + InterpolatorRectangular, +) +from autoarray.inversion.mesh.interpolator.delaunay import InterpolatorDelaunay +from autoarray.structures.triangles.shape import Circle +from autoarray.structures.triangles.shape import Triangle +from autoarray.structures.triangles.shape import Square +from autoarray.structures.triangles.shape import Polygon +from autoarray.structures.vectors.uniform import VectorYX2D +from autoarray.structures.vectors.irregular import VectorYX2DIrregular +from autoarray.structures.visibilities import Visibilities +from autoarray.structures.visibilities import VisibilitiesNoiseMap +from autoarray.inversion.mesh.mesh_geometry.rectangular import ( + rectangular_edge_pixel_list_from, +) + +from autogalaxy import cosmology as cosmo +from autogalaxy.analysis.adapt_images.adapt_images import AdaptImages +from autogalaxy.analysis.adapt_images.adapt_images import ( + galaxy_name_image_dict_via_result_from, +) +from autogalaxy.gui.clicker import Clicker +from autogalaxy.gui.scribbler import Scribbler +from autogalaxy.galaxy.galaxy import Galaxy +from autogalaxy.galaxy.galaxies import Galaxies +from autogalaxy.galaxy.galaxy_table import GalaxyTable +from autogalaxy.galaxy.galaxy_table import galaxy_table_from_csv +from autogalaxy.galaxy.galaxy_table import galaxy_table_to_csv +from autogalaxy.galaxy.galaxy_model_csv import GalaxyModelRow +from autogalaxy.galaxy.galaxy_model_csv import GalaxyModelTable +from autogalaxy.galaxy.galaxy_model_csv import galaxy_models_from_csv +from autogalaxy.galaxy.galaxy_model_csv import galaxy_models_to_csv +from autogalaxy.galaxy.galaxy_model_csv import galaxies_from_csv_tables +from autogalaxy.galaxy.galaxy_model_csv import galaxy_af_models_from_csv_tables +from autogalaxy.galaxy.redshift import Redshift + +from autogalaxy.profiles.geometry_profiles import EllProfile +from autogalaxy.profiles import ( + point_sources as ps, + mass as mp, + light_and_mass_profiles as lmp, + light_linear_and_mass_profiles as lmp_linear, + scaling_relations as sr, +) +from autogalaxy.profiles.light.abstract import LightProfile +from autogalaxy.profiles.light import standard as lp +from autogalaxy.profiles.light import snr as lp_snr +from autogalaxy.profiles.light import linear as lp_linear +from autogalaxy.profiles.light import operated as lp_operated +from autogalaxy.profiles import basis as lp_basis +from autogalaxy.profiles.light import ( + linear_operated as lp_linear_operated, +) +from autogalaxy.profiles.light.linear import LightProfileLinearObjFuncList +from autogalaxy.operate.image import OperateImage +from autogalaxy.operate.lens_calc import LensCalc +from autogalaxy import convert + +from . import aggregator as agg +from .analysis import model_util +from .lens import subhalo +from .lens.tracer import Tracer +from .lens.sensitivity import SubhaloSensitivityResult +from .lens.to_inversion import TracerToInversion +from .analysis.positions import PositionsLH +from .imaging.simulator import SimulatorImaging +from .imaging.fit_imaging import FitImaging +from .imaging.model.analysis import AnalysisImaging +from autofit import Latent +from .analysis.latent import LatentLens +from .imaging.model.visualizer import VisualizerImaging +from .interferometer.simulator import SimulatorInterferometer +from .interferometer.fit_interferometer import FitInterferometer +from .interferometer.model.analysis import AnalysisInterferometer +from .interferometer.model.visualizer import VisualizerInterferometer +from .point.dataset import PointDataset +from .point.dataset import list_from_csv +from .point.dataset import output_to_csv +from .point.fit.dataset import FitPointDataset +from .point.fit.fluxes import FitFluxes +from .point.fit.times_delays import FitTimeDelays +from .point.fit.positions.image.abstract import AbstractFitPositionsImagePair +from .point.fit.positions.image.pair import FitPositionsImagePair +from .point.fit.positions.image.pair_all import FitPositionsImagePairAll +from .point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat +from .point.fit.positions.source.separations import FitPositionsSource +from .point.max_separation import SourceMaxSeparation +from .point.model.analysis import AnalysisPoint +from .point.solver import PointSolver +from .point.solver.shape_solver import ShapeSolver +from .weak.dataset import WeakDataset +from .weak.fit import FitWeak +from .weak.model.analysis import AnalysisWeak +from .weak.simulator import SimulatorShearYX + +from . import exc +from . import mock as m +from . import util +from . import potential_correction as pc + +from autonerves import conf +from autonerves.fitsable import ndarray_via_hdu_from +from autonerves.fitsable import ndarray_via_fits_from +from autonerves.fitsable import header_obj_from +from autonerves.fitsable import output_to_fits +from autonerves.fitsable import hdu_list_for_output_from + +conf.instance.register(__file__) + +__version__ = "2026.7.23.1" + +from autonerves import check_version + +check_version(__version__) + + +def __getattr__(name): + if name == "plot": + from . import plot + + globals()["plot"] = plot + return plot + if name == "interop": + import importlib + + interop = importlib.import_module(f"{__name__}.interop") + globals()["interop"] = interop + return interop + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # --------------------------------------------------------------------------- # Public re-export of the autonerves configuration / serialization surface. diff --git a/autolens/aggregator/__init__.py b/autolens/aggregator/__init__.py index 5359d292b..91dc559ae 100644 --- a/autolens/aggregator/__init__.py +++ b/autolens/aggregator/__init__.py @@ -1,37 +1,37 @@ -from autogalaxy.aggregator.imaging.imaging import _imaging_from -from autogalaxy.aggregator.imaging.imaging import ImagingAgg - -from autogalaxy.aggregator.interferometer.interferometer import _interferometer_from -from autogalaxy.aggregator.interferometer.interferometer import InterferometerAgg - -from autolens.aggregator.tracer import _tracer_from -from autolens.aggregator.tracer import TracerAgg - -from autolens.aggregator.fit_imaging import _fit_imaging_from -from autolens.aggregator.fit_imaging import FitImagingAgg - -from autolens.aggregator.fit_interferometer import _fit_interferometer_from -from autolens.aggregator.fit_interferometer import FitInterferometerAgg - -from autogalaxy.aggregator.ellipse.ellipses import _ellipses_from -from autogalaxy.aggregator.ellipse.ellipses import EllipsesAgg -from autogalaxy.aggregator.ellipse.multipoles import _multipoles_from -from autogalaxy.aggregator.ellipse.multipoles import MultipolesAgg -from autogalaxy.aggregator.ellipse.fit_ellipse import _fit_ellipse_from -from autogalaxy.aggregator.ellipse.fit_ellipse import FitEllipseAgg - -from autolens.aggregator.subhalo import SubhaloAgg - -from autolens.aggregator.subplot import Dataset as subplot_dataset -from autolens.aggregator.subplot import Tracer as subplot_tracer -from autolens.aggregator.subplot import FitX1Plane as subplot_fit_x1_plane -from autolens.aggregator.subplot import Fit as subplot_fit -from autolens.aggregator.subplot import FitLog10 as subplot_fit_log10 -from autolens.aggregator.subplot import ( - FITSGalaxyImages as fits_galaxy_images, -) -from autolens.aggregator.subplot import ( - FITSModelGalaxyImages as fits_model_galaxy_images, -) -from autolens.aggregator.subplot import FITSTracer as fits_tracer -from autolens.aggregator.subplot import FITSFit as fits_fit +from autogalaxy.aggregator.imaging.imaging import _imaging_from +from autogalaxy.aggregator.imaging.imaging import ImagingAgg + +from autogalaxy.aggregator.interferometer.interferometer import _interferometer_from +from autogalaxy.aggregator.interferometer.interferometer import InterferometerAgg + +from autolens.aggregator.tracer import _tracer_from +from autolens.aggregator.tracer import TracerAgg + +from autolens.aggregator.fit_imaging import _fit_imaging_from +from autolens.aggregator.fit_imaging import FitImagingAgg + +from autolens.aggregator.fit_interferometer import _fit_interferometer_from +from autolens.aggregator.fit_interferometer import FitInterferometerAgg + +from autogalaxy.aggregator.ellipse.ellipses import _ellipses_from +from autogalaxy.aggregator.ellipse.ellipses import EllipsesAgg +from autogalaxy.aggregator.ellipse.multipoles import _multipoles_from +from autogalaxy.aggregator.ellipse.multipoles import MultipolesAgg +from autogalaxy.aggregator.ellipse.fit_ellipse import _fit_ellipse_from +from autogalaxy.aggregator.ellipse.fit_ellipse import FitEllipseAgg + +from autolens.aggregator.subhalo import SubhaloAgg + +from autolens.aggregator.subplot import Dataset as subplot_dataset +from autolens.aggregator.subplot import Tracer as subplot_tracer +from autolens.aggregator.subplot import FitX1Plane as subplot_fit_x1_plane +from autolens.aggregator.subplot import Fit as subplot_fit +from autolens.aggregator.subplot import FitLog10 as subplot_fit_log10 +from autolens.aggregator.subplot import ( + FITSGalaxyImages as fits_galaxy_images, +) +from autolens.aggregator.subplot import ( + FITSModelGalaxyImages as fits_model_galaxy_images, +) +from autolens.aggregator.subplot import FITSTracer as fits_tracer +from autolens.aggregator.subplot import FITSFit as fits_fit diff --git a/autolens/aggregator/fit_imaging.py b/autolens/aggregator/fit_imaging.py index 0f9a12e8f..855cade15 100644 --- a/autolens/aggregator/fit_imaging.py +++ b/autolens/aggregator/fit_imaging.py @@ -1,170 +1,170 @@ -""" -Aggregator interface for loading ``FitImaging`` objects from lens model-fit results. - -This module assembles ``FitImaging`` instances offline by combining the imaging dataset, -``Tracer``, dataset model, and adapt images stored in a ``PyAutoFit`` output directory or -SQLite database — reproducing exactly the fit that was evaluated during the original -non-linear search. - -Two public objects are provided: - -- ``_fit_imaging_from`` — a free function that accepts a single ``PyAutoFit`` ``Fit`` - entry and returns a list of ``FitImaging`` objects (one per summed ``Analysis``). -- ``FitImagingAgg`` — a ``PyAutoFit`` ``AggBase`` subclass wrapping an ``Aggregator`` - that exposes a generator of ``FitImaging`` objects, enabling memory-efficient iteration - over large result sets. -""" -from typing import Optional, List - -import autofit as af -import autoarray as aa - -from autolens.imaging.fit_imaging import FitImaging - -from autogalaxy.aggregator.imaging.imaging import _imaging_from -from autogalaxy.aggregator.dataset_model import _dataset_model_from -from autogalaxy.aggregator import agg_util - -from autolens.aggregator.tracer import _tracer_from - - -def _fit_imaging_from( - fit: af.Fit, - instance: Optional[af.ModelInstance] = None, - settings: aa.Settings = None, -) -> List[FitImaging]: - """ - Returns a list of `FitImaging` object from a `PyAutoFit` loaded directory `Fit` or sqlite database `Fit` object. - - The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following - attributes of the fit: - - - The imaging data, noise-map, PSF and settings as .fits files (e.g. `dataset/data.fits`). - - The mask used to mask the `Imaging` data structure in the fit (`dataset.fits[hdu=0]`). - - The settings of inversions used by the fit (`dataset/settings.json`). - - Each individual attribute can be loaded from the database via the `fit.value()` method. - - This method combines all of these attributes and returns a `FitImaging` object for a given non-linear search sample - (e.g. the maximum likelihood model). This includes associating adapt images with their respective galaxies. - - If multiple `FitImaging` objects were fitted simultaneously via analysis summing, the `fit.child_values()` method - is instead used to load lists of the data, noise-map, PSF and mask and combine them into a list of - `FitImaging` objects. - - The settings of an inversion can be overwritten by inputting a `settings` object, for example - if you want to use a grid with a different inversion solver. - - Parameters - ---------- - fit - A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from - an output directory or from an sqlite database.. - instance - A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance - randomly from the PDF). - settings - Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. - """ - - dataset_list = _imaging_from(fit=fit) - - tracer_list = _tracer_from(fit=fit, instance=instance) - - dataset_model_list = _dataset_model_from(fit=fit, instance=instance) - - adapt_images_list = agg_util.adapt_images_from(fit=fit) - - settings = settings or fit.value(name="settings") - - fit_dataset_list = [] - - for dataset, tracer, dataset_model, adapt_images in zip( - dataset_list, - tracer_list, - dataset_model_list, - adapt_images_list, - ): - fit_dataset_list.append( - FitImaging( - dataset=dataset, - tracer=tracer, - dataset_model=dataset_model, - adapt_images=adapt_images, - settings=settings, - ) - ) - - return fit_dataset_list - - -class FitImagingAgg(af.AggBase): - def __init__( - self, - aggregator: af.Aggregator, - settings: Optional[aa.Settings] = None, - ): - """ - Interfaces with an `PyAutoFit` aggregator object to create instances of `FitImaging` objects from the results - of a model-fit. - - The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following - attributes of the fit: - - - The imaging data, noise-map, PSF and settings as .fits files (e.g. `dataset/data.fits`). - - The mask used to mask the `Imaging` data structure in the fit (`dataset.fits[hdu=0]`). - - The settings of inversions used by the fit (`dataset/settings.json`). - - The `aggregator` contains the path to each of these files, and they can be loaded individually. This class - can load them all at once and create an `FitImaging` object via the `_fit_imaging_from` method. - - This class's methods returns generators which create the instances of the `FitImaging` objects. This ensures - that large sets of results can be efficiently loaded from the hard-disk and do not require storing all - `FitImaging` instances in the memory at once. - - For example, if the `aggregator` contains 3 model-fits, this class can be used to create a generator which - creates instances of the corresponding 3 `FitImaging` objects. - - If multiple `FitImaging` objects were fitted simultaneously via analysis summing, the `fit.child_values()` method - is instead used to load lists of the data, noise-map, PSF and mask and combine them into a list of - `FitImaging` objects. - - This can be done manually, but this object provides a more concise API. - - Parameters - ---------- - aggregator - A `PyAutoFit` aggregator object which can load the results of model-fits. - settings - Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. - use_preloaded_grid - Certain pixelization's construct their mesh in the source-plane from a stochastic KMeans algorithm. This - grid may be output to hard-disk after the model-fit and loaded via the database to ensure the same grid is - used as the fit. - """ - super().__init__(aggregator=aggregator) - - self.settings = settings - - def object_via_gen_from( - self, fit, instance: Optional[af.ModelInstance] = None - ) -> List[FitImaging]: - """ - Returns a generator of `FitImaging` objects from an input aggregator. - - See `__init__` for a description of how the `FitImaging` objects are created by this method. - - Parameters - ---------- - fit - A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from - an output directory or from an sqlite database.. - instance - A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance - randomly from the PDF). - """ - return _fit_imaging_from( - fit=fit, - instance=instance, - settings=self.settings, - ) +""" +Aggregator interface for loading ``FitImaging`` objects from lens model-fit results. + +This module assembles ``FitImaging`` instances offline by combining the imaging dataset, +``Tracer``, dataset model, and adapt images stored in a ``PyAutoFit`` output directory or +SQLite database — reproducing exactly the fit that was evaluated during the original +non-linear search. + +Two public objects are provided: + +- ``_fit_imaging_from`` — a free function that accepts a single ``PyAutoFit`` ``Fit`` + entry and returns a list of ``FitImaging`` objects (one per summed ``Analysis``). +- ``FitImagingAgg`` — a ``PyAutoFit`` ``AggBase`` subclass wrapping an ``Aggregator`` + that exposes a generator of ``FitImaging`` objects, enabling memory-efficient iteration + over large result sets. +""" +from typing import Optional, List + +import autofit as af +import autoarray as aa + +from autolens.imaging.fit_imaging import FitImaging + +from autogalaxy.aggregator.imaging.imaging import _imaging_from +from autogalaxy.aggregator.dataset_model import _dataset_model_from +from autogalaxy.aggregator import agg_util + +from autolens.aggregator.tracer import _tracer_from + + +def _fit_imaging_from( + fit: af.Fit, + instance: Optional[af.ModelInstance] = None, + settings: aa.Settings = None, +) -> List[FitImaging]: + """ + Returns a list of `FitImaging` object from a `PyAutoFit` loaded directory `Fit` or sqlite database `Fit` object. + + The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following + attributes of the fit: + + - The imaging data, noise-map, PSF and settings as .fits files (e.g. `dataset/data.fits`). + - The mask used to mask the `Imaging` data structure in the fit (`dataset.fits[hdu=0]`). + - The settings of inversions used by the fit (`dataset/settings.json`). + + Each individual attribute can be loaded from the database via the `fit.value()` method. + + This method combines all of these attributes and returns a `FitImaging` object for a given non-linear search sample + (e.g. the maximum likelihood model). This includes associating adapt images with their respective galaxies. + + If multiple `FitImaging` objects were fitted simultaneously via analysis summing, the `fit.child_values()` method + is instead used to load lists of the data, noise-map, PSF and mask and combine them into a list of + `FitImaging` objects. + + The settings of an inversion can be overwritten by inputting a `settings` object, for example + if you want to use a grid with a different inversion solver. + + Parameters + ---------- + fit + A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from + an output directory or from an sqlite database.. + instance + A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance + randomly from the PDF). + settings + Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. + """ + + dataset_list = _imaging_from(fit=fit) + + tracer_list = _tracer_from(fit=fit, instance=instance) + + dataset_model_list = _dataset_model_from(fit=fit, instance=instance) + + adapt_images_list = agg_util.adapt_images_from(fit=fit) + + settings = settings or fit.value(name="settings") + + fit_dataset_list = [] + + for dataset, tracer, dataset_model, adapt_images in zip( + dataset_list, + tracer_list, + dataset_model_list, + adapt_images_list, + ): + fit_dataset_list.append( + FitImaging( + dataset=dataset, + tracer=tracer, + dataset_model=dataset_model, + adapt_images=adapt_images, + settings=settings, + ) + ) + + return fit_dataset_list + + +class FitImagingAgg(af.AggBase): + def __init__( + self, + aggregator: af.Aggregator, + settings: Optional[aa.Settings] = None, + ): + """ + Interfaces with an `PyAutoFit` aggregator object to create instances of `FitImaging` objects from the results + of a model-fit. + + The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following + attributes of the fit: + + - The imaging data, noise-map, PSF and settings as .fits files (e.g. `dataset/data.fits`). + - The mask used to mask the `Imaging` data structure in the fit (`dataset.fits[hdu=0]`). + - The settings of inversions used by the fit (`dataset/settings.json`). + + The `aggregator` contains the path to each of these files, and they can be loaded individually. This class + can load them all at once and create an `FitImaging` object via the `_fit_imaging_from` method. + + This class's methods returns generators which create the instances of the `FitImaging` objects. This ensures + that large sets of results can be efficiently loaded from the hard-disk and do not require storing all + `FitImaging` instances in the memory at once. + + For example, if the `aggregator` contains 3 model-fits, this class can be used to create a generator which + creates instances of the corresponding 3 `FitImaging` objects. + + If multiple `FitImaging` objects were fitted simultaneously via analysis summing, the `fit.child_values()` method + is instead used to load lists of the data, noise-map, PSF and mask and combine them into a list of + `FitImaging` objects. + + This can be done manually, but this object provides a more concise API. + + Parameters + ---------- + aggregator + A `PyAutoFit` aggregator object which can load the results of model-fits. + settings + Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. + use_preloaded_grid + Certain pixelization's construct their mesh in the source-plane from a stochastic KMeans algorithm. This + grid may be output to hard-disk after the model-fit and loaded via the database to ensure the same grid is + used as the fit. + """ + super().__init__(aggregator=aggregator) + + self.settings = settings + + def object_via_gen_from( + self, fit, instance: Optional[af.ModelInstance] = None + ) -> List[FitImaging]: + """ + Returns a generator of `FitImaging` objects from an input aggregator. + + See `__init__` for a description of how the `FitImaging` objects are created by this method. + + Parameters + ---------- + fit + A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from + an output directory or from an sqlite database.. + instance + A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance + randomly from the PDF). + """ + return _fit_imaging_from( + fit=fit, + instance=instance, + settings=self.settings, + ) diff --git a/autolens/aggregator/fit_interferometer.py b/autolens/aggregator/fit_interferometer.py index 51295969d..ac6389050 100644 --- a/autolens/aggregator/fit_interferometer.py +++ b/autolens/aggregator/fit_interferometer.py @@ -1,167 +1,167 @@ -""" -Aggregator interface for loading ``FitInterferometer`` objects from lens model-fit results. - -This module assembles ``FitInterferometer`` instances offline by combining the -interferometer dataset, ``Tracer``, dataset model, and adapt images stored in a -``PyAutoFit`` output directory or SQLite database — reproducing exactly the fit that was -evaluated during the original non-linear search. - -Two public objects are provided: - -- ``_fit_interferometer_from`` — a free function that accepts a single ``PyAutoFit`` - ``Fit`` entry and returns a list of ``FitInterferometer`` objects (one per summed - ``Analysis``). -- ``FitInterferometerAgg`` — a ``PyAutoFit`` ``AggBase`` subclass wrapping an - ``Aggregator`` that exposes a generator of ``FitInterferometer`` objects, enabling - memory-efficient iteration over large result sets. -""" -from typing import Optional, List - -import autofit as af -import autoarray as aa - -from autogalaxy.aggregator.interferometer.interferometer import _interferometer_from -from autogalaxy.aggregator.dataset_model import _dataset_model_from - -from autolens.interferometer.fit_interferometer import FitInterferometer - -from autogalaxy.aggregator import agg_util -from autolens.aggregator.tracer import _tracer_from - - -def _fit_interferometer_from( - fit: af.Fit, - instance: Optional[af.ModelInstance] = None, - settings: aa.Settings = None, -) -> List[FitInterferometer]: - """ - Returns a list of `FitInterferometer` objects from a `PyAutoFit` loaded directory `Fit` or sqlite database `Fit` object. - - The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following - attributes of the fit: - - - The interferometer data, noise-map, uv-wavelengths and settings as .fits files (e.g. `dataset/data.fits`). - - The real space mask defining the grid of the interferometer for the FFT (`dataset/real_space_mask.fits`). - - The settings of inversions used by the fit (`dataset/settings.json`). - - Each individual attribute can be loaded from the database via the `fit.value()` method. - - This method combines all of these attributes and returns a `FitInterferometer` object for a given non-linear - search sample (e.g. the maximum likelihood model). This includes associating adapt images with their respective - galaxies. - - If multiple `FitInterferometer` objects were fitted simultaneously via analysis summing, the `fit.child_values()` - method is instead used to load lists of the data, noise-map, PSF and mask and combine them into a list of - `FitInterferometer` objects. - - The settings of an inversion can be overwritten by inputting a `settings` object, for - example if you want to use a grid with a different inversion solver. - - Parameters - ---------- - fit - A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from - an output directory or from an sqlite database.. - instance - A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance - randomly from the PDF). - settings - Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. - """ - dataset_list = _interferometer_from( - fit=fit, - ) - tracer_list = _tracer_from(fit=fit, instance=instance) - dataset_model_list = _dataset_model_from(fit=fit, instance=instance) - - adapt_images_list = agg_util.adapt_images_from(fit=fit) - - settings = settings or fit.value(name="settings") - - fit_dataset_list = [] - - for dataset, tracer, dataset_model, adapt_images in zip( - dataset_list, - tracer_list, - dataset_model_list, - adapt_images_list, - ): - fit_dataset_list.append( - FitInterferometer( - dataset=dataset, - tracer=tracer, - dataset_model=dataset_model, - adapt_images=adapt_images, - settings=settings, - ) - ) - - return fit_dataset_list - - -class FitInterferometerAgg(af.AggBase): - def __init__( - self, - aggregator: af.Aggregator, - settings: Optional[aa.Settings] = None, - ): - """ - Interfaces with an `PyAutoFit` aggregator object to create instances of `FitInterferometer` objects from the - results of a model-fit. - - The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following - attributes of the fit: - - - The interferometer data, noise-map, uv-wavelengths and settings as .fits files (e.g. `dataset/data.fits`). - - The real space mask defining the grid of the interferometer for the FFT (`dataset/real_space_mask.fits`). - - The settings of inversions used by the fit (`dataset/settings.json`). - - The `aggregator` contains the path to each of these files, and they can be loaded individually. This class - can load them all at once and create an `FitInterferometer` object via the `_fit_interferometer_from` method. - - This class's methods returns generators which create the instances of the `FitInterferometer` objects. This ensures - that large sets of results can be efficiently loaded from the hard-disk and do not require storing all - `FitInterferometer` instances in the memory at once. - - For example, if the `aggregator` contains 3 model-fits, this class can be used to create a generator which - creates instances of the corresponding 3 `FitInterferometer` objects. - - This can be done manually, but this object provides a more concise API. - - Parameters - ---------- - aggregator - A `PyAutoFit` aggregator object which can load the results of model-fits. - settings - Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. - use_preloaded_grid - Certain pixelization's construct their mesh in the source-plane from a stochastic KMeans algorithm. This - grid may be output to hard-disk after the model-fit and loaded via the database to ensure the same grid is - used as the fit. - """ - super().__init__(aggregator=aggregator) - - self.settings = settings - - def object_via_gen_from( - self, fit, instance: Optional[af.ModelInstance] = None - ) -> List[FitInterferometer]: - """ - Returns a generator of `FitInterferometer` objects from an input aggregator. - - See `__init__` for a description of how the `FitInterferometer` objects are created by this method. - - Parameters - ---------- - fit - A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from - an output directory or from an sqlite database.. - instance - A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance - randomly from the PDF). - """ - return _fit_interferometer_from( - fit=fit, - instance=instance, - settings=self.settings, - ) +""" +Aggregator interface for loading ``FitInterferometer`` objects from lens model-fit results. + +This module assembles ``FitInterferometer`` instances offline by combining the +interferometer dataset, ``Tracer``, dataset model, and adapt images stored in a +``PyAutoFit`` output directory or SQLite database — reproducing exactly the fit that was +evaluated during the original non-linear search. + +Two public objects are provided: + +- ``_fit_interferometer_from`` — a free function that accepts a single ``PyAutoFit`` + ``Fit`` entry and returns a list of ``FitInterferometer`` objects (one per summed + ``Analysis``). +- ``FitInterferometerAgg`` — a ``PyAutoFit`` ``AggBase`` subclass wrapping an + ``Aggregator`` that exposes a generator of ``FitInterferometer`` objects, enabling + memory-efficient iteration over large result sets. +""" +from typing import Optional, List + +import autofit as af +import autoarray as aa + +from autogalaxy.aggregator.interferometer.interferometer import _interferometer_from +from autogalaxy.aggregator.dataset_model import _dataset_model_from + +from autolens.interferometer.fit_interferometer import FitInterferometer + +from autogalaxy.aggregator import agg_util +from autolens.aggregator.tracer import _tracer_from + + +def _fit_interferometer_from( + fit: af.Fit, + instance: Optional[af.ModelInstance] = None, + settings: aa.Settings = None, +) -> List[FitInterferometer]: + """ + Returns a list of `FitInterferometer` objects from a `PyAutoFit` loaded directory `Fit` or sqlite database `Fit` object. + + The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following + attributes of the fit: + + - The interferometer data, noise-map, uv-wavelengths and settings as .fits files (e.g. `dataset/data.fits`). + - The real space mask defining the grid of the interferometer for the FFT (`dataset/real_space_mask.fits`). + - The settings of inversions used by the fit (`dataset/settings.json`). + + Each individual attribute can be loaded from the database via the `fit.value()` method. + + This method combines all of these attributes and returns a `FitInterferometer` object for a given non-linear + search sample (e.g. the maximum likelihood model). This includes associating adapt images with their respective + galaxies. + + If multiple `FitInterferometer` objects were fitted simultaneously via analysis summing, the `fit.child_values()` + method is instead used to load lists of the data, noise-map, PSF and mask and combine them into a list of + `FitInterferometer` objects. + + The settings of an inversion can be overwritten by inputting a `settings` object, for + example if you want to use a grid with a different inversion solver. + + Parameters + ---------- + fit + A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from + an output directory or from an sqlite database.. + instance + A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance + randomly from the PDF). + settings + Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. + """ + dataset_list = _interferometer_from( + fit=fit, + ) + tracer_list = _tracer_from(fit=fit, instance=instance) + dataset_model_list = _dataset_model_from(fit=fit, instance=instance) + + adapt_images_list = agg_util.adapt_images_from(fit=fit) + + settings = settings or fit.value(name="settings") + + fit_dataset_list = [] + + for dataset, tracer, dataset_model, adapt_images in zip( + dataset_list, + tracer_list, + dataset_model_list, + adapt_images_list, + ): + fit_dataset_list.append( + FitInterferometer( + dataset=dataset, + tracer=tracer, + dataset_model=dataset_model, + adapt_images=adapt_images, + settings=settings, + ) + ) + + return fit_dataset_list + + +class FitInterferometerAgg(af.AggBase): + def __init__( + self, + aggregator: af.Aggregator, + settings: Optional[aa.Settings] = None, + ): + """ + Interfaces with an `PyAutoFit` aggregator object to create instances of `FitInterferometer` objects from the + results of a model-fit. + + The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following + attributes of the fit: + + - The interferometer data, noise-map, uv-wavelengths and settings as .fits files (e.g. `dataset/data.fits`). + - The real space mask defining the grid of the interferometer for the FFT (`dataset/real_space_mask.fits`). + - The settings of inversions used by the fit (`dataset/settings.json`). + + The `aggregator` contains the path to each of these files, and they can be loaded individually. This class + can load them all at once and create an `FitInterferometer` object via the `_fit_interferometer_from` method. + + This class's methods returns generators which create the instances of the `FitInterferometer` objects. This ensures + that large sets of results can be efficiently loaded from the hard-disk and do not require storing all + `FitInterferometer` instances in the memory at once. + + For example, if the `aggregator` contains 3 model-fits, this class can be used to create a generator which + creates instances of the corresponding 3 `FitInterferometer` objects. + + This can be done manually, but this object provides a more concise API. + + Parameters + ---------- + aggregator + A `PyAutoFit` aggregator object which can load the results of model-fits. + settings + Optionally overwrite the `Settings` of the `Inversion` object that is created from the fit. + use_preloaded_grid + Certain pixelization's construct their mesh in the source-plane from a stochastic KMeans algorithm. This + grid may be output to hard-disk after the model-fit and loaded via the database to ensure the same grid is + used as the fit. + """ + super().__init__(aggregator=aggregator) + + self.settings = settings + + def object_via_gen_from( + self, fit, instance: Optional[af.ModelInstance] = None + ) -> List[FitInterferometer]: + """ + Returns a generator of `FitInterferometer` objects from an input aggregator. + + See `__init__` for a description of how the `FitInterferometer` objects are created by this method. + + Parameters + ---------- + fit + A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from + an output directory or from an sqlite database.. + instance + A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance + randomly from the PDF). + """ + return _fit_interferometer_from( + fit=fit, + instance=instance, + settings=self.settings, + ) diff --git a/autolens/aggregator/subhalo.py b/autolens/aggregator/subhalo.py index b2e422a8a..1e361c3da 100644 --- a/autolens/aggregator/subhalo.py +++ b/autolens/aggregator/subhalo.py @@ -1,47 +1,47 @@ -""" -Aggregator interface for dark-matter subhalo grid-search results. - -``SubhaloAgg`` wraps a ``PyAutoFit`` ``GridSearchAggregator`` to provide a convenient -interface for loading and comparing the results of a subhalo detection grid search. - -A subhalo grid search runs an independent non-linear search in each cell of an image-plane -grid, with the subhalo's (y, x) centre confined to that cell's region. ``SubhaloAgg`` -collates these per-cell results and exposes generators that yield ``FitImaging`` objects -for the best-fit model in each cell — both the "with subhalo" fit and (optionally) the -"no subhalo" baseline — enabling the per-cell log-evidence difference map to be computed. -""" -from typing import Optional - -import autofit as af -import autoarray as aa - -from autolens import exc - - -class SubhaloAgg: - def __init__( - self, - aggregator_grid_search: af.GridSearchAggregator, - settings: Optional[aa.Settings] = None, - ): - """ - Wraps a PyAutoFit aggregator in order to create generators of fits to imaging data, corresponding to the - results of a non-linear search model-fit. - """ - - self.aggregator_grid_search = aggregator_grid_search - self.settings = settings - - if len(aggregator_grid_search) == 0: - raise exc.AggregatorException( - "There is no grid search of results in the aggregator." - ) - elif len(aggregator_grid_search) > 1: - raise exc.AggregatorException( - "There is more than one grid search of results in the aggregator - please filter the" - "aggregator." - ) - - @property - def grid_search_result(self) -> af.GridSearchResult: - return self.aggregator_grid_search[0]["result"] +""" +Aggregator interface for dark-matter subhalo grid-search results. + +``SubhaloAgg`` wraps a ``PyAutoFit`` ``GridSearchAggregator`` to provide a convenient +interface for loading and comparing the results of a subhalo detection grid search. + +A subhalo grid search runs an independent non-linear search in each cell of an image-plane +grid, with the subhalo's (y, x) centre confined to that cell's region. ``SubhaloAgg`` +collates these per-cell results and exposes generators that yield ``FitImaging`` objects +for the best-fit model in each cell — both the "with subhalo" fit and (optionally) the +"no subhalo" baseline — enabling the per-cell log-evidence difference map to be computed. +""" +from typing import Optional + +import autofit as af +import autoarray as aa + +from autolens import exc + + +class SubhaloAgg: + def __init__( + self, + aggregator_grid_search: af.GridSearchAggregator, + settings: Optional[aa.Settings] = None, + ): + """ + Wraps a PyAutoFit aggregator in order to create generators of fits to imaging data, corresponding to the + results of a non-linear search model-fit. + """ + + self.aggregator_grid_search = aggregator_grid_search + self.settings = settings + + if len(aggregator_grid_search) == 0: + raise exc.AggregatorException( + "There is no grid search of results in the aggregator." + ) + elif len(aggregator_grid_search) > 1: + raise exc.AggregatorException( + "There is more than one grid search of results in the aggregator - please filter the" + "aggregator." + ) + + @property + def grid_search_result(self) -> af.GridSearchResult: + return self.aggregator_grid_search[0]["result"] diff --git a/autolens/aggregator/subplot.py b/autolens/aggregator/subplot.py index 0240d46d3..f8261e7f9 100644 --- a/autolens/aggregator/subplot.py +++ b/autolens/aggregator/subplot.py @@ -1,133 +1,133 @@ -from enum import Enum - - -class FITSGalaxyImages(Enum): - """ - The HDUs that can be extracted from the fit.fits file. - """ - - lens_light_image = "GALAXY_0" - lensed_source_image = "GALAXY_1" - - -class FITSModelGalaxyImages(Enum): - """ - The HDUs that can be extracted from the fit.fits file. - """ - - lens_light_image = "GALAXY_0" - lensed_source_image = "GALAXY_1" - - -class FITSTracer(Enum): - """ - The HDUs that can be extracted from the fit.fits file. - """ - - convergence = "CONVERGENCE" - potential = "POTENTIAL" - deflections_y = "DEFLECTIONS_Y" - deflections_x = "DEFLECTIONS_X" - - -class FITSFit(Enum): - """ - The HDUs that can be extracted from the fit.fits file. - """ - - model_data = "MODEL_DATA" - residual_map = "RESIDUAL_MAP" - normalized_residual_map = "NORMALIZED_RESIDUAL_MAP" - chi_squared_map = "CHI_SQUARED_MAP" - - -class Dataset(Enum): - """ - The subplots that can be extracted from the fit image. - - The values correspond to the position of the subplot in the 4x3 grid. - """ - - data = (0, 0) - data_log_10 = (1, 0) - noise_map = (2, 0) - psf = (0, 1) - psf_log_10 = (1, 1) - signal_to_noise_map = (2, 1) - over_sample_size_lp = (0, 2) - over_sample_size_pixelization = (1, 2) - - -class Tracer(Enum): - """ - The subplots that can be extracted from the tracer image. - - The values correspond to the position of the subplot in the 3x3 grid. - """ - - image = (0, 0) - source_image = (1, 0) - source_plane_image = (2, 0) - lens_light_image = (0, 1) - convergence = (1, 1) - potential = (2, 1) - magnification = (0, 2) - deflections_y = (1, 2) - deflections_x = (2, 2) - - -class FitX1Plane(Enum): - """ - The subplots that can be extracted from the fit image. - - The values correspond to the position of the subplot in the 4x3 grid. - """ - - data = (0, 0) - signal_to_noise_map = (1, 0) - model_data = (2, 0) - lens_light_subtracted_image = (0, 1) - lens_light_subtracted_image_zero = (1, 1) - normalized_residual_map = (2, 1) - - -class Fit(Enum): - """ - The subplots that can be extracted from the fit image. - - The values correspond to the position of the subplot in the 4x3 grid. - """ - - data = (0, 0) - data_source_scale = (1, 0) - signal_to_noise_map = (2, 0) - model_data = (3, 0) - lens_light_model = (0, 1) - lens_light_subtracted_image = (1, 1) - source_model_image = (2, 1) - source_plane_image_zoom = (3, 1) - normalized_residual_map = (0, 2) - normalized_residual_map_one_sigma = (1, 2) - chi_squared_map = (2, 2) - source_plane_image = (3, 2) - - -class FitLog10(Enum): - """ - The subplots that can be extracted from the fit image. - - The values correspond to the position of the subplot in the 4x3 grid. - """ - - data = (0, 0) - data_source_scale = (1, 0) - signal_to_noise_map = (2, 0) - model_data = (3, 0) - lens_light_model = (0, 1) - lens_light_subtracted_image = (1, 1) - source_model_image = (2, 1) - source_plane_image_zoom = (3, 1) - normalized_residual_map = (0, 2) - normalized_residual_map_one_sigma = (1, 2) - chi_squared_map = (2, 2) - source_plane_image = (3, 2) +from enum import Enum + + +class FITSGalaxyImages(Enum): + """ + The HDUs that can be extracted from the fit.fits file. + """ + + lens_light_image = "GALAXY_0" + lensed_source_image = "GALAXY_1" + + +class FITSModelGalaxyImages(Enum): + """ + The HDUs that can be extracted from the fit.fits file. + """ + + lens_light_image = "GALAXY_0" + lensed_source_image = "GALAXY_1" + + +class FITSTracer(Enum): + """ + The HDUs that can be extracted from the fit.fits file. + """ + + convergence = "CONVERGENCE" + potential = "POTENTIAL" + deflections_y = "DEFLECTIONS_Y" + deflections_x = "DEFLECTIONS_X" + + +class FITSFit(Enum): + """ + The HDUs that can be extracted from the fit.fits file. + """ + + model_data = "MODEL_DATA" + residual_map = "RESIDUAL_MAP" + normalized_residual_map = "NORMALIZED_RESIDUAL_MAP" + chi_squared_map = "CHI_SQUARED_MAP" + + +class Dataset(Enum): + """ + The subplots that can be extracted from the fit image. + + The values correspond to the position of the subplot in the 4x3 grid. + """ + + data = (0, 0) + data_log_10 = (1, 0) + noise_map = (2, 0) + psf = (0, 1) + psf_log_10 = (1, 1) + signal_to_noise_map = (2, 1) + over_sample_size_lp = (0, 2) + over_sample_size_pixelization = (1, 2) + + +class Tracer(Enum): + """ + The subplots that can be extracted from the tracer image. + + The values correspond to the position of the subplot in the 3x3 grid. + """ + + image = (0, 0) + source_image = (1, 0) + source_plane_image = (2, 0) + lens_light_image = (0, 1) + convergence = (1, 1) + potential = (2, 1) + magnification = (0, 2) + deflections_y = (1, 2) + deflections_x = (2, 2) + + +class FitX1Plane(Enum): + """ + The subplots that can be extracted from the fit image. + + The values correspond to the position of the subplot in the 4x3 grid. + """ + + data = (0, 0) + signal_to_noise_map = (1, 0) + model_data = (2, 0) + lens_light_subtracted_image = (0, 1) + lens_light_subtracted_image_zero = (1, 1) + normalized_residual_map = (2, 1) + + +class Fit(Enum): + """ + The subplots that can be extracted from the fit image. + + The values correspond to the position of the subplot in the 4x3 grid. + """ + + data = (0, 0) + data_source_scale = (1, 0) + signal_to_noise_map = (2, 0) + model_data = (3, 0) + lens_light_model = (0, 1) + lens_light_subtracted_image = (1, 1) + source_model_image = (2, 1) + source_plane_image_zoom = (3, 1) + normalized_residual_map = (0, 2) + normalized_residual_map_one_sigma = (1, 2) + chi_squared_map = (2, 2) + source_plane_image = (3, 2) + + +class FitLog10(Enum): + """ + The subplots that can be extracted from the fit image. + + The values correspond to the position of the subplot in the 4x3 grid. + """ + + data = (0, 0) + data_source_scale = (1, 0) + signal_to_noise_map = (2, 0) + model_data = (3, 0) + lens_light_model = (0, 1) + lens_light_subtracted_image = (1, 1) + source_model_image = (2, 1) + source_plane_image_zoom = (3, 1) + normalized_residual_map = (0, 2) + normalized_residual_map_one_sigma = (1, 2) + chi_squared_map = (2, 2) + source_plane_image = (3, 2) diff --git a/autolens/aggregator/tracer.py b/autolens/aggregator/tracer.py index 82c4d8cbf..7a246f31d 100644 --- a/autolens/aggregator/tracer.py +++ b/autolens/aggregator/tracer.py @@ -1,157 +1,157 @@ -""" -Aggregator interface for loading ``Tracer`` objects from model-fit results. - -After a **PyAutoLens** model-fit the best-fit galaxy model parameters are stored in the -output directory or SQLite database as part of the model JSON file. This module -reconstructs ``Tracer`` instances from those stored parameters so that the full -ray-tracing model can be re-evaluated and inspected without re-running the fit. - -Two public objects are provided: - -- ``_tracer_from`` — a free function that accepts a single ``PyAutoFit`` ``Fit`` entry - and returns the list of ``Tracer`` objects (one per summed ``Analysis``) for the - requested model instance. -- ``TracerAgg`` — a ``PyAutoFit`` ``AggBase`` subclass wrapping an ``Aggregator`` that - exposes a generator of tracer lists, one entry per stored model-fit. -""" -import logging -from typing import List, Optional - -import autofit as af -import autoarray as aa -import autogalaxy as ag - -from autolens.lens.tracer import Tracer - -from autogalaxy.aggregator import agg_util -from autolens.lens import tracer_util - -logger = logging.getLogger(__name__) - - -def _tracer_from( - fit: af.Fit, instance: Optional[af.ModelInstance] = None -) -> List[Tracer]: - """ - Returns a list of `Tracer` objects from a `PyAutoFit` loaded directory `Fit` or sqlite database `Fit` object. - - The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following - attributes of the fit: - - - The model and its best fit parameters (e.g. `model.json`). - - Each individual attribute can be loaded from the database via the `fit.value()` method. - - This method combines this attributesand returns a `Tracer` object for a given non-linear search sample - (e.g. the maximum likelihood model). - - If multiple `Tracer` objects were fitted simultaneously via multiple analysis, the instance is iterated over as - a list such that a list of `Tracer` objects with parameters updated for each analysis are returned. This means - fits using a single analysis are wrapped in a list to prodcue a consistent API. - - Parameters - ---------- - fit - A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from - an output directory or from an sqlite database.. - instance - A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance - randomly from the PDF). - """ - - instance_list = agg_util.instance_list_from(fit=fit, instance=instance) - - tracer_list = [] - - for instance in instance_list: - - try: - cosmology = instance.cosmology - except AttributeError: - cosmology = fit.value(name="cosmology") - - if cosmology is None: - cosmology = ag.cosmo.Planck15() - - # TODO : These are ugly as hell (>_<) - - if hasattr(instance, "perturb"): - instance.galaxies.subhalo = instance.perturb - - if hasattr(instance.galaxies, "subhalo"): - subhalo_centre = tracer_util.grid_2d_at_redshift_from( - galaxies=instance.galaxies, - redshift=instance.galaxies.subhalo.redshift, - grid=aa.Grid2DIrregular(values=[instance.galaxies.subhalo.mass.centre]), - cosmology=cosmology, - ) - - instance.galaxies.subhalo.mass.centre = tuple(subhalo_centre.in_list[0]) - - galaxy_list = list(instance.galaxies) - - if hasattr(instance, "extra_galaxies"): - if instance.extra_galaxies is not None: - galaxy_list += list(instance.extra_galaxies) - - if hasattr(instance, "scaling_galaxies"): - if instance.scaling_galaxies is not None: - galaxy_list += list(instance.scaling_galaxies) - - tracer = Tracer(galaxies=galaxy_list, cosmology=cosmology) - - tracer_list.append(tracer) - - return tracer_list - - -class TracerAgg(af.AggBase): - """ - Interfaces with an `PyAutoFit` aggregator object to create instances of `Tracer` objects from the results - of a model-fit. - - The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following - attributes of the fit: - - - The model and its best fit parameters (e.g. `model.json`). - - The adapt images associated with adaptive galaxy features (`adapt` folder). - - The `aggregator` contains the path to each of these files, and they can be loaded individually. This class - can load them all at once and create an `Tracer` object via the `_tracer_from` method. - - This class's methods returns generators which create the instances of the `Tracer` objects. This ensures - that large sets of results can be efficiently loaded from the hard-disk and do not require storing all - `Tracer` instances in the memory at once. - - For example, if the `aggregator` contains 3 model-fits, this class can be used to create a generator which - creates instances of the corresponding 3 `Tracer` objects. - - If multiple `Tracer` objects were fitted simultaneously via analysis summing, the `fit.child_values()` method - is instead used to load lists of Tracers. This is necessary if each Tracer has different galaxies (e.g. certain - parameters vary across each dataset and `Analysis` object). - - This can be done manually, but this object provides a more concise API. - - Parameters - ---------- - aggregator - A `PyAutoFit` aggregator object which can load the results of model-fits. - """ - - def object_via_gen_from( - self, fit, instance: Optional[af.ModelInstance] = None - ) -> List[Tracer]: - """ - Returns a generator of `Tracer` objects from an input aggregator. - - See `__init__` for a description of how the `Tracer` objects are created by this method. - - Parameters - ---------- - fit - A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from - an output directory or from an sqlite database.. - galaxies - A list of galaxies corresponding to a sample of a non-linear search and model-fit. - """ - return _tracer_from(fit=fit, instance=instance) +""" +Aggregator interface for loading ``Tracer`` objects from model-fit results. + +After a **PyAutoLens** model-fit the best-fit galaxy model parameters are stored in the +output directory or SQLite database as part of the model JSON file. This module +reconstructs ``Tracer`` instances from those stored parameters so that the full +ray-tracing model can be re-evaluated and inspected without re-running the fit. + +Two public objects are provided: + +- ``_tracer_from`` — a free function that accepts a single ``PyAutoFit`` ``Fit`` entry + and returns the list of ``Tracer`` objects (one per summed ``Analysis``) for the + requested model instance. +- ``TracerAgg`` — a ``PyAutoFit`` ``AggBase`` subclass wrapping an ``Aggregator`` that + exposes a generator of tracer lists, one entry per stored model-fit. +""" +import logging +from typing import List, Optional + +import autofit as af +import autoarray as aa +import autogalaxy as ag + +from autolens.lens.tracer import Tracer + +from autogalaxy.aggregator import agg_util +from autolens.lens import tracer_util + +logger = logging.getLogger(__name__) + + +def _tracer_from( + fit: af.Fit, instance: Optional[af.ModelInstance] = None +) -> List[Tracer]: + """ + Returns a list of `Tracer` objects from a `PyAutoFit` loaded directory `Fit` or sqlite database `Fit` object. + + The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following + attributes of the fit: + + - The model and its best fit parameters (e.g. `model.json`). + + Each individual attribute can be loaded from the database via the `fit.value()` method. + + This method combines this attributesand returns a `Tracer` object for a given non-linear search sample + (e.g. the maximum likelihood model). + + If multiple `Tracer` objects were fitted simultaneously via multiple analysis, the instance is iterated over as + a list such that a list of `Tracer` objects with parameters updated for each analysis are returned. This means + fits using a single analysis are wrapped in a list to prodcue a consistent API. + + Parameters + ---------- + fit + A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from + an output directory or from an sqlite database.. + instance + A manual instance that overwrites the max log likelihood instance in fit (e.g. for drawing the instance + randomly from the PDF). + """ + + instance_list = agg_util.instance_list_from(fit=fit, instance=instance) + + tracer_list = [] + + for instance in instance_list: + + try: + cosmology = instance.cosmology + except AttributeError: + cosmology = fit.value(name="cosmology") + + if cosmology is None: + cosmology = ag.cosmo.Planck15() + + # TODO : These are ugly as hell (>_<) + + if hasattr(instance, "perturb"): + instance.galaxies.subhalo = instance.perturb + + if hasattr(instance.galaxies, "subhalo"): + subhalo_centre = tracer_util.grid_2d_at_redshift_from( + galaxies=instance.galaxies, + redshift=instance.galaxies.subhalo.redshift, + grid=aa.Grid2DIrregular(values=[instance.galaxies.subhalo.mass.centre]), + cosmology=cosmology, + ) + + instance.galaxies.subhalo.mass.centre = tuple(subhalo_centre.in_list[0]) + + galaxy_list = list(instance.galaxies) + + if hasattr(instance, "extra_galaxies"): + if instance.extra_galaxies is not None: + galaxy_list += list(instance.extra_galaxies) + + if hasattr(instance, "scaling_galaxies"): + if instance.scaling_galaxies is not None: + galaxy_list += list(instance.scaling_galaxies) + + tracer = Tracer(galaxies=galaxy_list, cosmology=cosmology) + + tracer_list.append(tracer) + + return tracer_list + + +class TracerAgg(af.AggBase): + """ + Interfaces with an `PyAutoFit` aggregator object to create instances of `Tracer` objects from the results + of a model-fit. + + The results of a model-fit can be loaded from hard-disk or stored in a sqlite database, including the following + attributes of the fit: + + - The model and its best fit parameters (e.g. `model.json`). + - The adapt images associated with adaptive galaxy features (`adapt` folder). + + The `aggregator` contains the path to each of these files, and they can be loaded individually. This class + can load them all at once and create an `Tracer` object via the `_tracer_from` method. + + This class's methods returns generators which create the instances of the `Tracer` objects. This ensures + that large sets of results can be efficiently loaded from the hard-disk and do not require storing all + `Tracer` instances in the memory at once. + + For example, if the `aggregator` contains 3 model-fits, this class can be used to create a generator which + creates instances of the corresponding 3 `Tracer` objects. + + If multiple `Tracer` objects were fitted simultaneously via analysis summing, the `fit.child_values()` method + is instead used to load lists of Tracers. This is necessary if each Tracer has different galaxies (e.g. certain + parameters vary across each dataset and `Analysis` object). + + This can be done manually, but this object provides a more concise API. + + Parameters + ---------- + aggregator + A `PyAutoFit` aggregator object which can load the results of model-fits. + """ + + def object_via_gen_from( + self, fit, instance: Optional[af.ModelInstance] = None + ) -> List[Tracer]: + """ + Returns a generator of `Tracer` objects from an input aggregator. + + See `__init__` for a description of how the `Tracer` objects are created by this method. + + Parameters + ---------- + fit + A `PyAutoFit` `Fit` object which contains the results of a model-fit as an entry which has been loaded from + an output directory or from an sqlite database.. + galaxies + A list of galaxies corresponding to a sample of a non-linear search and model-fit. + """ + return _tracer_from(fit=fit, instance=instance) diff --git a/autolens/analysis/analysis/dataset.py b/autolens/analysis/analysis/dataset.py index 4198ab303..097e67d64 100644 --- a/autolens/analysis/analysis/dataset.py +++ b/autolens/analysis/analysis/dataset.py @@ -1,193 +1,193 @@ -""" -Abstract analysis class for fitting a ``Tracer`` to imaging or interferometer datasets. - -``AnalysisDataset`` is the common base class shared by ``AnalysisImaging`` and -``AnalysisInterferometer``. It combines the dataset-level machinery from the -``autogalaxy`` base class (``AgAnalysisDataset``) with the lensing-specific logic from -``AnalysisLens``: - -- Constructing a ``Tracer`` from a ``PyAutoFit`` model instance. -- Applying optional ``PositionsLH`` priors that penalise mass models where image positions - do not trace self-consistently to the source plane. -- Managing adaptive galaxy images (``AdaptImages``) for linear component fitting. -- Serialising results and running on-the-fly visualisation during the search. -""" -import logging -import numpy as np -from typing import List, Optional - -from autonerves import conf -from autonerves.dictable import output_to_json - -import autofit as af -import autoarray as aa -import autogalaxy as ag - -from autogalaxy.analysis.analysis.dataset import AnalysisDataset as AgAnalysisDataset - -from autolens.analysis.analysis.lens import AnalysisLens -from autolens.analysis.result import ResultDataset -from autolens.analysis.positions import PositionsLH - -from autolens import exc -from autonerves.test_mode import skip_checks - -logger = logging.getLogger(__name__) - -logger.setLevel(level="INFO") - - -class AnalysisDataset(AgAnalysisDataset, AnalysisLens): - def __init__( - self, - dataset, - positions_likelihood_list: Optional[List[PositionsLH]] = None, - adapt_images: Optional[ag.AdaptImages] = None, - cosmology: ag.cosmo.LensingCosmology = None, - settings: aa.Settings = None, - raise_inversion_positions_likelihood_exception: bool = True, - title_prefix: str = None, - use_jax: bool = True, - **kwargs, - ): - """ - Fits a lens model to a dataset via a non-linear search. - - This abstract Analysis class has attributes and methods for all model-fits which fit the model to a dataset - (e.g. imaging or interferometer data). - - This class stores the Cosmology used for the analysis and settings that control aspects of the calculation, - including how pixelizations, inversions and lensing calculations are performed. - - Parameters - ---------- - dataset - The imaging, interferometer or other dataset that the model if fitted too. - positions_likelihood_list - Alters the likelihood function to include a term which accounts for whether image-pixel coordinates in - arc-seconds corresponding to the multiple images of each lensed source galaxy trace close to one another in - their source-plane. This is a list, as it may support multiple planes, where a positions likelihood object - is input for each plane (e.g. double source plane lensing). - adapt_images - Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the - reconstructed galaxy's morphology. - cosmology - The Cosmology assumed for this analysis. - settings - Settings controlling how an inversion is fitted during the model-fit, for example which linear algebra - formalism is used. - raise_inversion_positions_likelihood_exception - If an inversion is used without the `positions_likelihood_list` it is likely a systematic solution will - be inferred, in which case an Exception is raised before the model-fit begins to inform the user - of this. This exception is not raised if this input is False, allowing the user to perform the model-fit - anyway. - """ - - super().__init__( - dataset=dataset, - adapt_images=adapt_images, - cosmology=cosmology, - settings=settings, - title_prefix=title_prefix, - use_jax=use_jax, - **kwargs, - ) - - # `super().__init__` routes through `af.Analysis.__init__`, the single - # reader of the disable-jax env var and the jax-availability check, which - # resolves `self._use_jax`. Forward that resolved value so `AnalysisLens` - # never overwrites it with the raw parameter. - AnalysisLens.__init__( - self=self, - positions_likelihood_list=positions_likelihood_list, - cosmology=cosmology, - use_jax=self._use_jax, - ) - - self.raise_inversion_positions_likelihood_exception = ( - raise_inversion_positions_likelihood_exception - ) - - if skip_checks(): - self.raise_inversion_positions_likelihood_exception = False - - def modify_before_fit(self, paths: af.DirectoryPaths, model: af.Collection): - """ - This function is called immediately before the non-linear search begins and performs final tasks and checks - before it begins. - - This function: - - - Checks that the adapt-dataset is consistent with previous adapt-datasets if the model-fit is being - resumed from a previous run. - - - Checks the model and raises exceptions if certain critieria are not met. - - Once inherited from it also visualizes objects which do not change throughout the model fit like the dataset. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization and the pickled objects used by the aggregator output by this function. - model - The model object, which includes model components representing the galaxies that are fitted to - the imaging data. - """ - - super().modify_before_fit(paths=paths, model=model) - - self.raise_exceptions(model=model) - - return self - - def raise_exceptions(self, model): - has_pix = model.has_model(cls=(aa.Pixelization,)) or model.has_instance( - cls=(aa.Pixelization,) - ) - - if has_pix: - if ( - self.positions_likelihood_list is None - and self.raise_inversion_positions_likelihood_exception - and not conf.instance["general"]["test"][ - "disable_positions_lh_inversion_check" - ] - ): - raise exc.AnalysisException( - """ - You have begun a model-fit which reconstructs the source using a pixelization. - However, you have not input a `positions_likelihood_list` object. - It is likely your model-fit will infer an inaccurate solution. - - Please read the following readthedocs page for a description of why this is, and how to set up - a positions likelihood object: - - https://pyautolens.readthedocs.io/en/latest/general/demagnified_solutions.html - """ - ) - - def save_results(self, paths: af.DirectoryPaths, result: ResultDataset): - """ - At the end of a model-fit, this routine saves attributes of the `Analysis` object to the `files` - folder such that they can be loaded after the analysis using PyAutoFit's database and aggregator tools. - - For this analysis it outputs the following: - - - The maximum log likelihood tracer of the fit. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization and the pickled objects used by the aggregator output by this function. - result - The result of a model fit, including the non-linear search, samples and maximum likelihood tracer. - """ - try: - output_to_json( - obj=result.max_log_likelihood_tracer, - file_path=paths._files_path / "tracer.json", - ) - except AttributeError: - pass +""" +Abstract analysis class for fitting a ``Tracer`` to imaging or interferometer datasets. + +``AnalysisDataset`` is the common base class shared by ``AnalysisImaging`` and +``AnalysisInterferometer``. It combines the dataset-level machinery from the +``autogalaxy`` base class (``AgAnalysisDataset``) with the lensing-specific logic from +``AnalysisLens``: + +- Constructing a ``Tracer`` from a ``PyAutoFit`` model instance. +- Applying optional ``PositionsLH`` priors that penalise mass models where image positions + do not trace self-consistently to the source plane. +- Managing adaptive galaxy images (``AdaptImages``) for linear component fitting. +- Serialising results and running on-the-fly visualisation during the search. +""" +import logging +import numpy as np +from typing import List, Optional + +from autonerves import conf +from autonerves.dictable import output_to_json + +import autofit as af +import autoarray as aa +import autogalaxy as ag + +from autogalaxy.analysis.analysis.dataset import AnalysisDataset as AgAnalysisDataset + +from autolens.analysis.analysis.lens import AnalysisLens +from autolens.analysis.result import ResultDataset +from autolens.analysis.positions import PositionsLH + +from autolens import exc +from autonerves.test_mode import skip_checks + +logger = logging.getLogger(__name__) + +logger.setLevel(level="INFO") + + +class AnalysisDataset(AgAnalysisDataset, AnalysisLens): + def __init__( + self, + dataset, + positions_likelihood_list: Optional[List[PositionsLH]] = None, + adapt_images: Optional[ag.AdaptImages] = None, + cosmology: ag.cosmo.LensingCosmology = None, + settings: aa.Settings = None, + raise_inversion_positions_likelihood_exception: bool = True, + title_prefix: str = None, + use_jax: bool = True, + **kwargs, + ): + """ + Fits a lens model to a dataset via a non-linear search. + + This abstract Analysis class has attributes and methods for all model-fits which fit the model to a dataset + (e.g. imaging or interferometer data). + + This class stores the Cosmology used for the analysis and settings that control aspects of the calculation, + including how pixelizations, inversions and lensing calculations are performed. + + Parameters + ---------- + dataset + The imaging, interferometer or other dataset that the model if fitted too. + positions_likelihood_list + Alters the likelihood function to include a term which accounts for whether image-pixel coordinates in + arc-seconds corresponding to the multiple images of each lensed source galaxy trace close to one another in + their source-plane. This is a list, as it may support multiple planes, where a positions likelihood object + is input for each plane (e.g. double source plane lensing). + adapt_images + Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the + reconstructed galaxy's morphology. + cosmology + The Cosmology assumed for this analysis. + settings + Settings controlling how an inversion is fitted during the model-fit, for example which linear algebra + formalism is used. + raise_inversion_positions_likelihood_exception + If an inversion is used without the `positions_likelihood_list` it is likely a systematic solution will + be inferred, in which case an Exception is raised before the model-fit begins to inform the user + of this. This exception is not raised if this input is False, allowing the user to perform the model-fit + anyway. + """ + + super().__init__( + dataset=dataset, + adapt_images=adapt_images, + cosmology=cosmology, + settings=settings, + title_prefix=title_prefix, + use_jax=use_jax, + **kwargs, + ) + + # `super().__init__` routes through `af.Analysis.__init__`, the single + # reader of the disable-jax env var and the jax-availability check, which + # resolves `self._use_jax`. Forward that resolved value so `AnalysisLens` + # never overwrites it with the raw parameter. + AnalysisLens.__init__( + self=self, + positions_likelihood_list=positions_likelihood_list, + cosmology=cosmology, + use_jax=self._use_jax, + ) + + self.raise_inversion_positions_likelihood_exception = ( + raise_inversion_positions_likelihood_exception + ) + + if skip_checks(): + self.raise_inversion_positions_likelihood_exception = False + + def modify_before_fit(self, paths: af.DirectoryPaths, model: af.Collection): + """ + This function is called immediately before the non-linear search begins and performs final tasks and checks + before it begins. + + This function: + + - Checks that the adapt-dataset is consistent with previous adapt-datasets if the model-fit is being + resumed from a previous run. + + - Checks the model and raises exceptions if certain critieria are not met. + + Once inherited from it also visualizes objects which do not change throughout the model fit like the dataset. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + model + The model object, which includes model components representing the galaxies that are fitted to + the imaging data. + """ + + super().modify_before_fit(paths=paths, model=model) + + self.raise_exceptions(model=model) + + return self + + def raise_exceptions(self, model): + has_pix = model.has_model(cls=(aa.Pixelization,)) or model.has_instance( + cls=(aa.Pixelization,) + ) + + if has_pix: + if ( + self.positions_likelihood_list is None + and self.raise_inversion_positions_likelihood_exception + and not conf.instance["general"]["test"][ + "disable_positions_lh_inversion_check" + ] + ): + raise exc.AnalysisException( + """ + You have begun a model-fit which reconstructs the source using a pixelization. + However, you have not input a `positions_likelihood_list` object. + It is likely your model-fit will infer an inaccurate solution. + + Please read the following readthedocs page for a description of why this is, and how to set up + a positions likelihood object: + + https://pyautolens.readthedocs.io/en/latest/general/demagnified_solutions.html + """ + ) + + def save_results(self, paths: af.DirectoryPaths, result: ResultDataset): + """ + At the end of a model-fit, this routine saves attributes of the `Analysis` object to the `files` + folder such that they can be loaded after the analysis using PyAutoFit's database and aggregator tools. + + For this analysis it outputs the following: + + - The maximum log likelihood tracer of the fit. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + result + The result of a model fit, including the non-linear search, samples and maximum likelihood tracer. + """ + try: + output_to_json( + obj=result.max_log_likelihood_tracer, + file_path=paths._files_path / "tracer.json", + ) + except AttributeError: + pass diff --git a/autolens/analysis/analysis/lens.py b/autolens/analysis/analysis/lens.py index 03d558f85..8a10aec33 100644 --- a/autolens/analysis/analysis/lens.py +++ b/autolens/analysis/analysis/lens.py @@ -1,181 +1,181 @@ -""" -Lensing-specific mixin for all **PyAutoLens** ``Analysis`` classes. - -``AnalysisLens`` is a mixin that adds lensing-specific behaviour to any analysis class. -It is inherited (alongside dataset-specific base classes) by ``AnalysisDataset`` -and ``AnalysisPoint``. - -Key responsibilities: - -- ``tracer_via_instance_from`` — constructs a ``Tracer`` from a ``PyAutoFit`` model - instance, including automatic multi-plane ordering of galaxies by redshift. -- Position likelihood application — evaluates ``PositionsLH`` objects against the - current tracer and adds any penalty to the log likelihood. -- ``use_jax`` flag — forwarded to ``FitImaging`` / ``FitInterferometer`` to enable JAX - acceleration of the likelihood evaluation. -""" -import logging -import numpy as np -from typing import List, Optional - -import autofit as af -import autoarray as aa -import autogalaxy as ag - -from autolens.analysis.positions import PositionsLH -from autolens.lens.tracer import Tracer - -from autolens.lens import tracer_util - -from autolens import exc - -logger = logging.getLogger(__name__) - -logger.setLevel(level="INFO") - - -class AnalysisLens: - def __init__( - self, - positions_likelihood_list: Optional[List[PositionsLH]] = None, - cosmology: ag.cosmo.LensingCosmology = None, - use_jax: bool = True, - ): - """ - Analysis classes are used by PyAutoFit to fit a model to a dataset via a non-linear search. - - This abstract Analysis class has attributes and methods for all model-fits which include lensing calculations, - but does not perform a model-fit by itself (and is therefore only inherited from). - - This class stores the Cosmology used for the analysis and settings that control specific aspects of the lensing - calculation, for example how close the brightest pixels in the lensed source have to trace within one another - in the source plane for the model to not be discarded. - - Parameters - ---------- - cosmology - The Cosmology assumed for this analysis. - """ - from autogalaxy.cosmology.model import Planck15 - - self.cosmology = cosmology or Planck15() - self.positions_likelihood_list = positions_likelihood_list - - # `use_jax` is expected to already be the base-resolved value - # (`self._use_jax` set by `af.Analysis.__init__`, the single reader of - # the disable-jax env var and the jax-availability check). This guard is - # a defensive, idempotent re-check of jax availability only — it never - # re-reads the env var and does not repeat the parent's loud banner. - import importlib.util - if use_jax and importlib.util.find_spec("jax") is None: - use_jax = False - - self._use_jax = use_jax - - @property - def _xp(self): - if self._use_jax: - import jax.numpy as jnp - - return jnp - return np - - def tracer_via_instance_from( - self, - instance: af.ModelInstance, - ) -> Tracer: - """ - Create a `Tracer` from the galaxies contained in a model instance. - - Parameters - ---------- - instance - An instance of the model that is fitted to the data by this analysis (whose parameters may have been set - via a non-linear search). - - Returns - ------- - Tracer - An instance of the Tracer class that is used to then fit the dataset. - """ - if hasattr(instance, "perturb"): - instance.galaxies.subhalo = instance.perturb - - # TODO : Need to think about how we do this without building it into the model attribute names. - # TODO : A Subhalo class that extends the Galaxy class maybe? - - if hasattr(instance.galaxies, "subhalo"): - subhalo_centre = tracer_util.grid_2d_at_redshift_from( - galaxies=instance.galaxies, - redshift=instance.galaxies.subhalo.redshift, - grid=aa.Grid2DIrregular( - values=[instance.galaxies.subhalo.mass.centre], xp=self._xp - ), - cosmology=self.cosmology, - xp=self._xp, - ) - - instance.galaxies.subhalo.mass.centre = tuple(subhalo_centre.in_list[0]) - - if hasattr(instance, "cosmology"): - cosmology = instance.cosmology - else: - cosmology = self.cosmology - - galaxy_list = list(instance.galaxies) - - if hasattr(instance, "extra_galaxies"): - if getattr(instance, "extra_galaxies", None) is not None: - galaxy_list += list(instance.extra_galaxies) - - if hasattr(instance, "scaling_galaxies"): - if getattr(instance, "scaling_galaxies", None) is not None: - galaxy_list += list(instance.scaling_galaxies) - - return Tracer( - galaxies=galaxy_list, - cosmology=cosmology, - ) - - def log_likelihood_penalty_from( - self, - instance: af.ModelInstance, - ) -> Optional[float]: - """ - Call the positions overwrite log likelihood function, which add a penalty term to the likelihood if the - positions of the multiple images of the lensed source do not trace close to one another in the - source plane. - - This function handles a number of exceptions which may occur when calling the overwrite function via the - `PositionsLikelihood` class, so that they do not need to be handled individually for each `Analysis` class. - - Parameters - ---------- - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - - Returns - ------- - The penalty value of the positions log likelihood, if the positions do not trace close in the source plane, - else a None is returned to indicate there is no penalty. - """ - log_likelihood_penalty = self._xp.array(0.0) - - if self.positions_likelihood_list is not None: - - for positions_likelihood in self.positions_likelihood_list: - - if positions_likelihood is not None: - - log_likelihood_penalty = ( - positions_likelihood.log_likelihood_penalty_from( - instance=instance, analysis=self, xp=self._xp - ) - ) - - log_likelihood_penalty += log_likelihood_penalty - - return log_likelihood_penalty - - return log_likelihood_penalty +""" +Lensing-specific mixin for all **PyAutoLens** ``Analysis`` classes. + +``AnalysisLens`` is a mixin that adds lensing-specific behaviour to any analysis class. +It is inherited (alongside dataset-specific base classes) by ``AnalysisDataset`` +and ``AnalysisPoint``. + +Key responsibilities: + +- ``tracer_via_instance_from`` — constructs a ``Tracer`` from a ``PyAutoFit`` model + instance, including automatic multi-plane ordering of galaxies by redshift. +- Position likelihood application — evaluates ``PositionsLH`` objects against the + current tracer and adds any penalty to the log likelihood. +- ``use_jax`` flag — forwarded to ``FitImaging`` / ``FitInterferometer`` to enable JAX + acceleration of the likelihood evaluation. +""" +import logging +import numpy as np +from typing import List, Optional + +import autofit as af +import autoarray as aa +import autogalaxy as ag + +from autolens.analysis.positions import PositionsLH +from autolens.lens.tracer import Tracer + +from autolens.lens import tracer_util + +from autolens import exc + +logger = logging.getLogger(__name__) + +logger.setLevel(level="INFO") + + +class AnalysisLens: + def __init__( + self, + positions_likelihood_list: Optional[List[PositionsLH]] = None, + cosmology: ag.cosmo.LensingCosmology = None, + use_jax: bool = True, + ): + """ + Analysis classes are used by PyAutoFit to fit a model to a dataset via a non-linear search. + + This abstract Analysis class has attributes and methods for all model-fits which include lensing calculations, + but does not perform a model-fit by itself (and is therefore only inherited from). + + This class stores the Cosmology used for the analysis and settings that control specific aspects of the lensing + calculation, for example how close the brightest pixels in the lensed source have to trace within one another + in the source plane for the model to not be discarded. + + Parameters + ---------- + cosmology + The Cosmology assumed for this analysis. + """ + from autogalaxy.cosmology.model import Planck15 + + self.cosmology = cosmology or Planck15() + self.positions_likelihood_list = positions_likelihood_list + + # `use_jax` is expected to already be the base-resolved value + # (`self._use_jax` set by `af.Analysis.__init__`, the single reader of + # the disable-jax env var and the jax-availability check). This guard is + # a defensive, idempotent re-check of jax availability only — it never + # re-reads the env var and does not repeat the parent's loud banner. + import importlib.util + if use_jax and importlib.util.find_spec("jax") is None: + use_jax = False + + self._use_jax = use_jax + + @property + def _xp(self): + if self._use_jax: + import jax.numpy as jnp + + return jnp + return np + + def tracer_via_instance_from( + self, + instance: af.ModelInstance, + ) -> Tracer: + """ + Create a `Tracer` from the galaxies contained in a model instance. + + Parameters + ---------- + instance + An instance of the model that is fitted to the data by this analysis (whose parameters may have been set + via a non-linear search). + + Returns + ------- + Tracer + An instance of the Tracer class that is used to then fit the dataset. + """ + if hasattr(instance, "perturb"): + instance.galaxies.subhalo = instance.perturb + + # TODO : Need to think about how we do this without building it into the model attribute names. + # TODO : A Subhalo class that extends the Galaxy class maybe? + + if hasattr(instance.galaxies, "subhalo"): + subhalo_centre = tracer_util.grid_2d_at_redshift_from( + galaxies=instance.galaxies, + redshift=instance.galaxies.subhalo.redshift, + grid=aa.Grid2DIrregular( + values=[instance.galaxies.subhalo.mass.centre], xp=self._xp + ), + cosmology=self.cosmology, + xp=self._xp, + ) + + instance.galaxies.subhalo.mass.centre = tuple(subhalo_centre.in_list[0]) + + if hasattr(instance, "cosmology"): + cosmology = instance.cosmology + else: + cosmology = self.cosmology + + galaxy_list = list(instance.galaxies) + + if hasattr(instance, "extra_galaxies"): + if getattr(instance, "extra_galaxies", None) is not None: + galaxy_list += list(instance.extra_galaxies) + + if hasattr(instance, "scaling_galaxies"): + if getattr(instance, "scaling_galaxies", None) is not None: + galaxy_list += list(instance.scaling_galaxies) + + return Tracer( + galaxies=galaxy_list, + cosmology=cosmology, + ) + + def log_likelihood_penalty_from( + self, + instance: af.ModelInstance, + ) -> Optional[float]: + """ + Call the positions overwrite log likelihood function, which add a penalty term to the likelihood if the + positions of the multiple images of the lensed source do not trace close to one another in the + source plane. + + This function handles a number of exceptions which may occur when calling the overwrite function via the + `PositionsLikelihood` class, so that they do not need to be handled individually for each `Analysis` class. + + Parameters + ---------- + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + + Returns + ------- + The penalty value of the positions log likelihood, if the positions do not trace close in the source plane, + else a None is returned to indicate there is no penalty. + """ + log_likelihood_penalty = self._xp.array(0.0) + + if self.positions_likelihood_list is not None: + + for positions_likelihood in self.positions_likelihood_list: + + if positions_likelihood is not None: + + log_likelihood_penalty = ( + positions_likelihood.log_likelihood_penalty_from( + instance=instance, analysis=self, xp=self._xp + ) + ) + + log_likelihood_penalty += log_likelihood_penalty + + return log_likelihood_penalty + + return log_likelihood_penalty diff --git a/autolens/analysis/model_util.py b/autolens/analysis/model_util.py index eee39d306..aeca624f2 100644 --- a/autolens/analysis/model_util.py +++ b/autolens/analysis/model_util.py @@ -1,121 +1,121 @@ -""" -Model construction utilities for **PyAutoLens** example scripts and pipelines. - -This module provides convenience functions that build pre-configured ``af.Model`` -objects for common lens modeling scenarios. They are primarily intended for use in -the autolens_workspace ``start_here.py`` scripts and SLaM pipeline templates, where -a sensible default model is needed without the user having to specify every prior -explicitly. - -Key functions re-exported from ``autogalaxy``: -- ``mge_model_from`` — build an MGE (Multi-Gaussian Expansion) light profile model. -- ``mge_point_model_from`` — MGE model for point-source fitting. -- ``hilbert_pixels_from_pixel_scale`` — estimate Hilbert image-mesh pixel count. - -PyAutoLens-specific: -- ``random_galaxies_for_simulation_from`` — sample concrete (lens, source) ``Galaxy`` - instances for synthetic-data generation in ``start_here`` scripts. -""" -from typing import Optional, Tuple - -import numpy as np - -import autolens as al - -from autogalaxy.analysis.model_util import mge_model_from -from autogalaxy.analysis.model_util import mge_point_model_from -from autogalaxy.analysis.model_util import hilbert_pixels_from_pixel_scale - - -SIMULATOR_RANDOM_LENS_SUMMARY = ( - "Each simulated strong lens draws fresh truths from: " - "lens bulge SNR in [20, 60] (when included), " - "lens mass einstein_radius in [0.2, 1.8] with normal-clipped ellipticity, " - "external shear ~ Normal(0, 0.05), " - "source bulge SNR in [10, 30] / point-source flux in [0.0, 2.0] (mode dependent)." -) - - -def _clipped_ell_comp(rng: np.random.Generator) -> float: - return float(np.clip(rng.normal(0.0, 0.2), -1.0, 1.0)) - - -def random_galaxies_for_simulation_from( - include_lens_light: bool = True, - use_point_source: bool = False, - rng: Optional[np.random.Generator] = None, -) -> Tuple["al.Galaxy", "al.Galaxy"]: - """ - Sample a ``(lens_galaxy, source_galaxy)`` pair for synthetic strong-lens - data generation. - - Each parameter is drawn directly from a numpy ``Generator`` and used to - construct concrete profile instances — no ``af.Model`` priors are involved. - SNR-normalised Sersic profiles (``lp_snr.Sersic``) are used for diffuse - light components so that simulator output lands at a controlled target - SNR; the SNR appears as a profile attribute on the *instance*, never as a - fitting parameter. - - Do **not** use the returned galaxies as fitting models. They are - instances, suitable for ``Tracer`` / ``simulator.via_tracer_from``. - - Parameters - ---------- - include_lens_light - If True (default), give the lens galaxy an ``lp_snr.Sersic`` bulge. - If False, the lens is mass-only. - use_point_source - If True, source is a ``PointFlux`` with random centre and flux. If - False (default), source is an ``lp_snr.Sersic``. - rng - Optional ``numpy.random.Generator``. If ``None`` a fresh - ``default_rng()`` is created on each call. - - Returns - ------- - (Galaxy, Galaxy) - ``(lens_galaxy, source_galaxy)`` at redshifts 0.5 and 1.0 respectively. - """ - rng = rng if rng is not None else np.random.default_rng() - - if include_lens_light: - lens_bulge = al.lp_snr.Sersic( - centre=(0.0, 0.0), - ell_comps=(_clipped_ell_comp(rng), _clipped_ell_comp(rng)), - effective_radius=float(rng.uniform(1.0, 5.0)), - sersic_index=float(rng.uniform(3.5, 4.5)), - signal_to_noise_ratio=float(rng.uniform(20.0, 60.0)), - ) - else: - lens_bulge = None - - mass = al.mp.Isothermal( - centre=(0.0, 0.0), - ell_comps=(_clipped_ell_comp(rng), _clipped_ell_comp(rng)), - einstein_radius=float(rng.uniform(0.2, 1.8)), - ) - - shear = al.mp.ExternalShear( - gamma_1=float(rng.normal(0.0, 0.05)), - gamma_2=float(rng.normal(0.0, 0.05)), - ) - - lens = al.Galaxy(redshift=0.5, bulge=lens_bulge, mass=mass, shear=shear) - - if use_point_source: - point_0 = al.ps.PointFlux( - centre=(float(rng.normal(0.0, 0.3)), float(rng.normal(0.0, 0.3))), - flux=float(rng.uniform(0.0, 2.0)), - ) - source = al.Galaxy(redshift=1.0, point_0=point_0) - else: - source_bulge = al.lp_snr.Sersic( - centre=(float(rng.normal(0.0, 0.3)), float(rng.normal(0.0, 0.3))), - ell_comps=(_clipped_ell_comp(rng), _clipped_ell_comp(rng)), - effective_radius=float(rng.uniform(0.01, 3.0)), - sersic_index=float(rng.uniform(1.5, 2.5)), - signal_to_noise_ratio=float(rng.uniform(10.0, 30.0)), - ) - source = al.Galaxy(redshift=1.0, bulge=source_bulge) - - return lens, source +""" +Model construction utilities for **PyAutoLens** example scripts and pipelines. + +This module provides convenience functions that build pre-configured ``af.Model`` +objects for common lens modeling scenarios. They are primarily intended for use in +the autolens_workspace ``start_here.py`` scripts and SLaM pipeline templates, where +a sensible default model is needed without the user having to specify every prior +explicitly. + +Key functions re-exported from ``autogalaxy``: +- ``mge_model_from`` — build an MGE (Multi-Gaussian Expansion) light profile model. +- ``mge_point_model_from`` — MGE model for point-source fitting. +- ``hilbert_pixels_from_pixel_scale`` — estimate Hilbert image-mesh pixel count. + +PyAutoLens-specific: +- ``random_galaxies_for_simulation_from`` — sample concrete (lens, source) ``Galaxy`` + instances for synthetic-data generation in ``start_here`` scripts. +""" +from typing import Optional, Tuple + +import numpy as np + +import autolens as al + +from autogalaxy.analysis.model_util import mge_model_from +from autogalaxy.analysis.model_util import mge_point_model_from +from autogalaxy.analysis.model_util import hilbert_pixels_from_pixel_scale + + +SIMULATOR_RANDOM_LENS_SUMMARY = ( + "Each simulated strong lens draws fresh truths from: " + "lens bulge SNR in [20, 60] (when included), " + "lens mass einstein_radius in [0.2, 1.8] with normal-clipped ellipticity, " + "external shear ~ Normal(0, 0.05), " + "source bulge SNR in [10, 30] / point-source flux in [0.0, 2.0] (mode dependent)." +) + + +def _clipped_ell_comp(rng: np.random.Generator) -> float: + return float(np.clip(rng.normal(0.0, 0.2), -1.0, 1.0)) + + +def random_galaxies_for_simulation_from( + include_lens_light: bool = True, + use_point_source: bool = False, + rng: Optional[np.random.Generator] = None, +) -> Tuple["al.Galaxy", "al.Galaxy"]: + """ + Sample a ``(lens_galaxy, source_galaxy)`` pair for synthetic strong-lens + data generation. + + Each parameter is drawn directly from a numpy ``Generator`` and used to + construct concrete profile instances — no ``af.Model`` priors are involved. + SNR-normalised Sersic profiles (``lp_snr.Sersic``) are used for diffuse + light components so that simulator output lands at a controlled target + SNR; the SNR appears as a profile attribute on the *instance*, never as a + fitting parameter. + + Do **not** use the returned galaxies as fitting models. They are + instances, suitable for ``Tracer`` / ``simulator.via_tracer_from``. + + Parameters + ---------- + include_lens_light + If True (default), give the lens galaxy an ``lp_snr.Sersic`` bulge. + If False, the lens is mass-only. + use_point_source + If True, source is a ``PointFlux`` with random centre and flux. If + False (default), source is an ``lp_snr.Sersic``. + rng + Optional ``numpy.random.Generator``. If ``None`` a fresh + ``default_rng()`` is created on each call. + + Returns + ------- + (Galaxy, Galaxy) + ``(lens_galaxy, source_galaxy)`` at redshifts 0.5 and 1.0 respectively. + """ + rng = rng if rng is not None else np.random.default_rng() + + if include_lens_light: + lens_bulge = al.lp_snr.Sersic( + centre=(0.0, 0.0), + ell_comps=(_clipped_ell_comp(rng), _clipped_ell_comp(rng)), + effective_radius=float(rng.uniform(1.0, 5.0)), + sersic_index=float(rng.uniform(3.5, 4.5)), + signal_to_noise_ratio=float(rng.uniform(20.0, 60.0)), + ) + else: + lens_bulge = None + + mass = al.mp.Isothermal( + centre=(0.0, 0.0), + ell_comps=(_clipped_ell_comp(rng), _clipped_ell_comp(rng)), + einstein_radius=float(rng.uniform(0.2, 1.8)), + ) + + shear = al.mp.ExternalShear( + gamma_1=float(rng.normal(0.0, 0.05)), + gamma_2=float(rng.normal(0.0, 0.05)), + ) + + lens = al.Galaxy(redshift=0.5, bulge=lens_bulge, mass=mass, shear=shear) + + if use_point_source: + point_0 = al.ps.PointFlux( + centre=(float(rng.normal(0.0, 0.3)), float(rng.normal(0.0, 0.3))), + flux=float(rng.uniform(0.0, 2.0)), + ) + source = al.Galaxy(redshift=1.0, point_0=point_0) + else: + source_bulge = al.lp_snr.Sersic( + centre=(float(rng.normal(0.0, 0.3)), float(rng.normal(0.0, 0.3))), + ell_comps=(_clipped_ell_comp(rng), _clipped_ell_comp(rng)), + effective_radius=float(rng.uniform(0.01, 3.0)), + sersic_index=float(rng.uniform(1.5, 2.5)), + signal_to_noise_ratio=float(rng.uniform(10.0, 30.0)), + ) + source = al.Galaxy(redshift=1.0, bulge=source_bulge) + + return lens, source diff --git a/autolens/analysis/positions.py b/autolens/analysis/positions.py index 6916d74c0..21e1fe7b7 100644 --- a/autolens/analysis/positions.py +++ b/autolens/analysis/positions.py @@ -1,219 +1,219 @@ -""" -Position-based likelihood penalties for strong lens model fitting. - -``PositionsLH`` objects add a penalty term to the log likelihood whenever the observed -image-plane positions of a lensed point source do not self-consistently trace back to the -same source-plane location in the current model. - -The check is performed by tracing each observed position through the tracer's deflection -map and computing the maximum pairwise separation in the source plane -(``SourceMaxSeparation``). If this separation exceeds the user-specified ``threshold``, -a large negative penalty is added to the log likelihood: - - penalty = -log_likelihood_penalty_factor × (max_separation − threshold) - -This forces the non-linear search to explore models where the image positions are -self-consistent, greatly improving convergence speed for systems with strong position -constraints (e.g. quad-lens quasars). -""" -import numpy as np -from typing import Optional -from pathlib import Path - -import autoarray as aa -import autofit as af - -from autofit.tools.util import open_ - -import autogalaxy as ag - -from autogalaxy.analysis.analysis.dataset import AnalysisDataset - -from autolens.lens.tracer import Tracer -from autolens.point.max_separation import ( - SourceMaxSeparation, -) - -from autolens import exc - - -class PositionsLH: - def __init__( - self, - positions: aa.Grid2DIrregular, - threshold: float, - log_likelihood_penalty_factor: float = 1e8, - plane_redshift: Optional[float] = None, - ): - """ - The `PositionsLH` objects add a penalty term to the likelihood of the **PyAutoLens** `log_likelihood_function` - defined in the `Analysis` classes. - - The penalty term inspects the distance that the locations of the multiple images of the lensed source galaxy - trace within one another in the source-plane and penalizes solutions where they trace far from one another, - on the basis that this indicates an unphysical or inaccurate mass model. If they trace within the - threshold the penalty term is not applied. - - For the `PositionsLH` object, if the multiple image coordinates do not trace within the source-plane - threshold of one another a penalty to the likelihood is applied: - - `log_Likelihood_penalty_base - log_likelihood_penalty_factor * (max_source_plane_separation - threshold)` - - The penalty term reduces as the source-plane coordinates trace closer to one another, meaning that the - initial stages of the non-linear search can sample mass models that reduce the threshold. - - For example, for one penalty term, if the multiple image coordinates are defined - via `positions=aa.Grid2DIrregular([(1.0, 0.0), (-1.0, 0.0)]` and they do not trace within `threshold=0.3` of - one another, the mass model will receive a large likelihood penalty. - - The default behaviour assumes a single lens plane and single source plane, meaning the input `positions` - are the image-plane coordinates of one source galaxy at a specifc redshift. - - For multiple source planes, the `plane_redshift` can be used to pair the image-plane positions with the - redshift of the source plane they trace too. - - Parameters - ---------- - positions - The arcsecond coordinates of the lensed source multiple images which are used to compute the likelihood - penalty. - threshold - If the maximum separation of any two source plane coordinates is above the threshold the penalty term - is applied. - log_likelihood_penalty_factor - A factor which multiplies how far source pixels do not trace within the threshold of one another, with a - larger factor producing a larger penalty making the non-linear parameter space gradient steeper. - plane_redshift - The plane redshift of the lensed source multiple images, which is only required if position threshold - for a double source plane lens system is being used where the specific plane is required. - """ - - self.positions = positions - self.threshold = threshold - self.plane_redshift = plane_redshift - - if len(positions) == 1: - raise exc.PositionsException( - f"The positions input into the PositionsLikelihood object have length one " - f"(e.g. it is only one (y,x) coordinate and therefore cannot be compared with other images).\n\n" - "Please input more positions into the Positions." - ) - - self.log_likelihood_penalty_factor = log_likelihood_penalty_factor - - def output_positions_info( - self, output_path: str, tracer: Tracer, overwrite_file: bool = True - ): - """ - Outputs a `positions.info` file which summarises the positions penalty term for a model fit, including: - - - The arc second coordinates of the lensed source multiple images used for the model-fit. - - The radial distance of these coordinates from (0.0, 0.0). - - The threshold value used by the likelihood penalty. - - The maximum source plane separation of the maximum likelihood tracer. - - Parameters - ---------- - output_path - tracer - - Returns - ------- - - """ - - flag = "w+" if overwrite_file else "a+" - - with open_(Path(output_path) / "positions.info", flag) as f: - - positions_fit = SourceMaxSeparation( - data=self.positions, - noise_map=None, - tracer=tracer, - plane_redshift=self.plane_redshift, - ) - - distances = positions_fit.data.distances_to_coordinate_from( - coordinate=(0.0, 0.0) - ) - - if self.plane_redshift is None: - f.write(f"Plane Index: -1 \n") - else: - f.write(f"Plane Redshift: {self.plane_redshift} \n") - - f.write(f"Positions: \n {self.positions} \n\n") - f.write(f"Radial Distance from (0.0, 0.0): \n {distances} \n\n") - f.write(f"Threshold = {self.threshold} \n") - f.write( - f"Max Source Plane Separation of Maximum Likelihood Model = {positions_fit.max_separation_of_plane_positions}" - ) - f.write("") - - def log_likelihood_penalty_from( - self, instance: af.ModelInstance, analysis: AnalysisDataset, xp=np - ) -> np.array: - """ - Returns a log-likelihood penalty used to constrain lens models where multiple image-plane - positions do not trace to within a threshold distance of one another in the source-plane. - - This penalty is intended for use in `Analysis` classes that include the `PenaltyLH` mixin. It adds a - heavy penalty to the likelihood when the multiple images traces far apart in the source-plane, discouraging - models where the mapped source-plane positions are too widely separated. - - Specifically, if the maximum separation between traced positions in the source-plane exceeds - a defined threshold, a penalty term is applied to the log likelihood: - - penalty = log_likelihood_penalty_factor * (max_separation - threshold) - - If the separation is within the threshold, no penalty is applied. - - JAX Compatibility - ----------------- - Because this function may be jitted or differentiated using JAX, it uses `jax.lax.cond` to apply - conditional logic in a way that is compatible with JAX's functional and tracing model. - Both branches (penalty and zero) are evaluated at trace time, though only one is returned - at runtime depending on the condition. - - Parameters - ---------- - instance - The current model instance evaluated during the non-linear search. - analysis - The `Analysis` object calling this function, from which the `tracer` and `dataset` are derived. - - Returns - ------- - penalty - A scalar log-likelihood penalty (≥ 0) if the max separation exceeds the threshold, or 0.0 otherwise. - """ - tracer = analysis.tracer_via_instance_from(instance=instance) - - if not tracer.has(cls=ag.mp.MassProfile) or len(tracer.planes) == 1: - return xp.array(0.0) - - positions_fit = SourceMaxSeparation( - data=self.positions, - noise_map=None, - tracer=tracer, - plane_redshift=self.plane_redshift, - xp=xp, - ) - - max_separation = xp.max( - positions_fit.furthest_separations_of_plane_positions.array - ) - - penalty = self.log_likelihood_penalty_factor * (max_separation - self.threshold) - - if xp.__name__.startswith("jax"): - - import jax - - return jax.lax.cond( - max_separation > self.threshold, - lambda: penalty, - lambda: xp.array(0.0), - ) - - return penalty if max_separation > self.threshold else np.array(0.0) +""" +Position-based likelihood penalties for strong lens model fitting. + +``PositionsLH`` objects add a penalty term to the log likelihood whenever the observed +image-plane positions of a lensed point source do not self-consistently trace back to the +same source-plane location in the current model. + +The check is performed by tracing each observed position through the tracer's deflection +map and computing the maximum pairwise separation in the source plane +(``SourceMaxSeparation``). If this separation exceeds the user-specified ``threshold``, +a large negative penalty is added to the log likelihood: + + penalty = -log_likelihood_penalty_factor × (max_separation − threshold) + +This forces the non-linear search to explore models where the image positions are +self-consistent, greatly improving convergence speed for systems with strong position +constraints (e.g. quad-lens quasars). +""" +import numpy as np +from typing import Optional +from pathlib import Path + +import autoarray as aa +import autofit as af + +from autofit.tools.util import open_ + +import autogalaxy as ag + +from autogalaxy.analysis.analysis.dataset import AnalysisDataset + +from autolens.lens.tracer import Tracer +from autolens.point.max_separation import ( + SourceMaxSeparation, +) + +from autolens import exc + + +class PositionsLH: + def __init__( + self, + positions: aa.Grid2DIrregular, + threshold: float, + log_likelihood_penalty_factor: float = 1e8, + plane_redshift: Optional[float] = None, + ): + """ + The `PositionsLH` objects add a penalty term to the likelihood of the **PyAutoLens** `log_likelihood_function` + defined in the `Analysis` classes. + + The penalty term inspects the distance that the locations of the multiple images of the lensed source galaxy + trace within one another in the source-plane and penalizes solutions where they trace far from one another, + on the basis that this indicates an unphysical or inaccurate mass model. If they trace within the + threshold the penalty term is not applied. + + For the `PositionsLH` object, if the multiple image coordinates do not trace within the source-plane + threshold of one another a penalty to the likelihood is applied: + + `log_Likelihood_penalty_base - log_likelihood_penalty_factor * (max_source_plane_separation - threshold)` + + The penalty term reduces as the source-plane coordinates trace closer to one another, meaning that the + initial stages of the non-linear search can sample mass models that reduce the threshold. + + For example, for one penalty term, if the multiple image coordinates are defined + via `positions=aa.Grid2DIrregular([(1.0, 0.0), (-1.0, 0.0)]` and they do not trace within `threshold=0.3` of + one another, the mass model will receive a large likelihood penalty. + + The default behaviour assumes a single lens plane and single source plane, meaning the input `positions` + are the image-plane coordinates of one source galaxy at a specifc redshift. + + For multiple source planes, the `plane_redshift` can be used to pair the image-plane positions with the + redshift of the source plane they trace too. + + Parameters + ---------- + positions + The arcsecond coordinates of the lensed source multiple images which are used to compute the likelihood + penalty. + threshold + If the maximum separation of any two source plane coordinates is above the threshold the penalty term + is applied. + log_likelihood_penalty_factor + A factor which multiplies how far source pixels do not trace within the threshold of one another, with a + larger factor producing a larger penalty making the non-linear parameter space gradient steeper. + plane_redshift + The plane redshift of the lensed source multiple images, which is only required if position threshold + for a double source plane lens system is being used where the specific plane is required. + """ + + self.positions = positions + self.threshold = threshold + self.plane_redshift = plane_redshift + + if len(positions) == 1: + raise exc.PositionsException( + f"The positions input into the PositionsLikelihood object have length one " + f"(e.g. it is only one (y,x) coordinate and therefore cannot be compared with other images).\n\n" + "Please input more positions into the Positions." + ) + + self.log_likelihood_penalty_factor = log_likelihood_penalty_factor + + def output_positions_info( + self, output_path: str, tracer: Tracer, overwrite_file: bool = True + ): + """ + Outputs a `positions.info` file which summarises the positions penalty term for a model fit, including: + + - The arc second coordinates of the lensed source multiple images used for the model-fit. + - The radial distance of these coordinates from (0.0, 0.0). + - The threshold value used by the likelihood penalty. + - The maximum source plane separation of the maximum likelihood tracer. + + Parameters + ---------- + output_path + tracer + + Returns + ------- + + """ + + flag = "w+" if overwrite_file else "a+" + + with open_(Path(output_path) / "positions.info", flag) as f: + + positions_fit = SourceMaxSeparation( + data=self.positions, + noise_map=None, + tracer=tracer, + plane_redshift=self.plane_redshift, + ) + + distances = positions_fit.data.distances_to_coordinate_from( + coordinate=(0.0, 0.0) + ) + + if self.plane_redshift is None: + f.write(f"Plane Index: -1 \n") + else: + f.write(f"Plane Redshift: {self.plane_redshift} \n") + + f.write(f"Positions: \n {self.positions} \n\n") + f.write(f"Radial Distance from (0.0, 0.0): \n {distances} \n\n") + f.write(f"Threshold = {self.threshold} \n") + f.write( + f"Max Source Plane Separation of Maximum Likelihood Model = {positions_fit.max_separation_of_plane_positions}" + ) + f.write("") + + def log_likelihood_penalty_from( + self, instance: af.ModelInstance, analysis: AnalysisDataset, xp=np + ) -> np.array: + """ + Returns a log-likelihood penalty used to constrain lens models where multiple image-plane + positions do not trace to within a threshold distance of one another in the source-plane. + + This penalty is intended for use in `Analysis` classes that include the `PenaltyLH` mixin. It adds a + heavy penalty to the likelihood when the multiple images traces far apart in the source-plane, discouraging + models where the mapped source-plane positions are too widely separated. + + Specifically, if the maximum separation between traced positions in the source-plane exceeds + a defined threshold, a penalty term is applied to the log likelihood: + + penalty = log_likelihood_penalty_factor * (max_separation - threshold) + + If the separation is within the threshold, no penalty is applied. + + JAX Compatibility + ----------------- + Because this function may be jitted or differentiated using JAX, it uses `jax.lax.cond` to apply + conditional logic in a way that is compatible with JAX's functional and tracing model. + Both branches (penalty and zero) are evaluated at trace time, though only one is returned + at runtime depending on the condition. + + Parameters + ---------- + instance + The current model instance evaluated during the non-linear search. + analysis + The `Analysis` object calling this function, from which the `tracer` and `dataset` are derived. + + Returns + ------- + penalty + A scalar log-likelihood penalty (≥ 0) if the max separation exceeds the threshold, or 0.0 otherwise. + """ + tracer = analysis.tracer_via_instance_from(instance=instance) + + if not tracer.has(cls=ag.mp.MassProfile) or len(tracer.planes) == 1: + return xp.array(0.0) + + positions_fit = SourceMaxSeparation( + data=self.positions, + noise_map=None, + tracer=tracer, + plane_redshift=self.plane_redshift, + xp=xp, + ) + + max_separation = xp.max( + positions_fit.furthest_separations_of_plane_positions.array + ) + + penalty = self.log_likelihood_penalty_factor * (max_separation - self.threshold) + + if xp.__name__.startswith("jax"): + + import jax + + return jax.lax.cond( + max_separation > self.threshold, + lambda: penalty, + lambda: xp.array(0.0), + ) + + return penalty if max_separation > self.threshold else np.array(0.0) diff --git a/autolens/conf.py b/autolens/conf.py index d3f5a12fa..8b1378917 100644 --- a/autolens/conf.py +++ b/autolens/conf.py @@ -1 +1 @@ - + diff --git a/autolens/config/general.yaml b/autolens/config/general.yaml index 75e85a7dd..32229bf70 100644 --- a/autolens/config/general.yaml +++ b/autolens/config/general.yaml @@ -1,4 +1,4 @@ -output: - fit_dill: false -test: - disable_positions_lh_inversion_check: false +output: + fit_dill: false +test: + disable_positions_lh_inversion_check: false diff --git a/autolens/config/non_linear.yaml b/autolens/config/non_linear.yaml index d9356cf2d..c56ad38a0 100644 --- a/autolens/config/non_linear.yaml +++ b/autolens/config/non_linear.yaml @@ -1,57 +1,57 @@ -nest: - DynestyDynamic: - initialize: - method: prior - parallel: - force_x1_cpu: false - number_of_cores: 1 - printing: - silence: false - run: - dlogz_init: 0.01 - logl_max_init: .inf - maxcall: null - maxcall_init: null - maxiter: null - maxiter_init: null - n_effective: .inf - n_effective_init: .inf - nlive_init: 500 - search: - bootstrap: null - bound: multi - enlarge: null - facc: 0.2 - first_update: null - fmove: 0.9 - max_move: 100 - sample: rwalk - slices: 5 - update_interval: null - walks: 5 - DynestyStatic: - initialize: - method: prior - parallel: - number_of_cores: 1 - printing: - silence: false - run: - dlogz: null - logl_max: .inf - maxcall: null - maxiter: null - n_effective: null - search: - bootstrap: null - bound: multi - enlarge: null - facc: 0.2 - first_update: null - fmove: 0.9 - max_move: 100 - nlive: 50 - sample: rwalk - slices: 5 - update_interval: null - walks: 5 +nest: + DynestyDynamic: + initialize: + method: prior + parallel: + force_x1_cpu: false + number_of_cores: 1 + printing: + silence: false + run: + dlogz_init: 0.01 + logl_max_init: .inf + maxcall: null + maxcall_init: null + maxiter: null + maxiter_init: null + n_effective: .inf + n_effective_init: .inf + nlive_init: 500 + search: + bootstrap: null + bound: multi + enlarge: null + facc: 0.2 + first_update: null + fmove: 0.9 + max_move: 100 + sample: rwalk + slices: 5 + update_interval: null + walks: 5 + DynestyStatic: + initialize: + method: prior + parallel: + number_of_cores: 1 + printing: + silence: false + run: + dlogz: null + logl_max: .inf + maxcall: null + maxiter: null + n_effective: null + search: + bootstrap: null + bound: multi + enlarge: null + facc: 0.2 + first_update: null + fmove: 0.9 + max_move: 100 + nlive: 50 + sample: rwalk + slices: 5 + update_interval: null + walks: 5 diff --git a/autolens/config/visualize/plots.yaml b/autolens/config/visualize/plots.yaml index f49a599a2..5b78b606d 100644 --- a/autolens/config/visualize/plots.yaml +++ b/autolens/config/visualize/plots.yaml @@ -1,71 +1,71 @@ -# The `plots` section customizes every image that is output to hard-disk during a model-fit. - -# For example, if `plots: fit: subplot_fit=True``, the ``subplot_fit.png`` subplot file will -# be plotted every time visualization is performed. - -# There are two settings which are important for inspecting results via the dataset after a fit is complete which are: - -# - `fits_dataset`: This outputs `dataset.fits` which the database functionality may use to reperform fits. -# -`fits_adapt_images`, This outputs `adapt_images.fits` which the database functionality may use to reperform fits. - -# These can be disabled to save on hard-disk space but will lead to certain database functionality being disabled. - -subplot_format: [png] # Output format of all subplots, can be png, pdf or both (e.g. [png, pdf]) -fits_are_zoomed: false # If true, output .fits files are zoomed in on the center of the unmasked region image, saving hard-disk space. - -dataset: # Settings for plots of all datasets (e.g. Imaging, Interferometer). - subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the data, noise-map, etc.)? - fits_dataset: true # Output a .fits file containing the dataset data, noise-map and other quantities? - -positions: # Settings for plots with resampling image-positions on (e.g. the image). - image_with_positions: true - -fit: # Settings for plots of all fits (e.g. FitImaging, FitInterferometer). - subplot_fit: true # Plot subplot of all fit quantities for any dataset (e.g. the model data, residual-map, etc.)? - subplot_fit_log10: false # Plot subplot of all fit quantities for any dataset using log10 color maps (e.g. the model data, residual-map, etc.)? - subplot_of_planes: false # Plot subplot of the model-image, subtracted image and other quantities of each plane? - subplot_galaxies_images: false # Plot subplot of the image of each plane in the model? - fits_fit: true # Output a .fits file containing the fit model data, residual map, normalized residual map and chi-squared? - fits_galaxy_images : true # Output a .fits file containing the images (e.g. without PSF convolution) of every galaxy? - fits_model_galaxy_images : true # Output a .fits file containing the model images (e.g. with PSF convolution) of every galaxy? - -fit_imaging: {} # Settings for plots of fits to imaging datasets (e.g. FitImaging). - -tracer: # Settings for plots of tracers (e.g. Tracer). - subplot_tracer: true # Plot subplot of all quantities in each tracer (e.g. images, convergence)? - subplot_galaxies_images: false # Plot subplot of the image of each plane in the tracer? - fits_tracer: true # Output tracer.fits file of tracer's convergence, potential, deflections_y and deflections_x? - fits_source_plane_images: true # Output source_plane_images.fits file of the source-plane image (light profiles only) of each galaxy in the tracer? - fits_source_plane_shape: (100, 100) # The shape of the source-plane image output in the fits_source_plane_images.fits file. - -inversion: # Settings for plots of inversions (e.g. InversionPlotter). - subplot_inversion: true # Plot subplot of all quantities in each inversion (e.g. reconstrucuted image, reconstruction)? - subplot_mappings: false # Plot subplot of the image-to-source pixels mappings of each pixelization? - csv_reconstruction: true # output source_plane_reconstruction_0.csv containing the source-plane mesh y, x, reconstruction and noise map values. - -adapt: # Settings for plots of adapt images used by adaptive pixelizations. - subplot_adapt_images: true # Plot subplot showing each adapt image used for adaptive pixelization? - fits_adapt_images: true # Output a .fits file containing the adapt images used for adaptive pixelization? - -fit_interferometer: # Settings for plots of fits to interferometer datasets (e.g. FitInterferometer). - subplot_fit_dirty_images: false # Plot subplot of the dirty-images of all interferometer datasets? - subplot_fit_real_space: false # Plot subplot of the real-space images of all interferometer datasets? - fits_dirty_images: true # output dirty_images.fits showing the dirty image, noise-map, model-data, resiual-map, normalized residual map and chi-squared map? - -point_dataset: # Settings for plots of point source datasets (e.g. PointDatasetPlotter). - subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the data, noise-map, etc.)? - -fit_point_dataset: {} # Settings for plots of fits to point source datasets (e.g. FitPointDatasetPlotter). - -weak_dataset: # Settings for plots of weak lensing shear catalogues (e.g. PlotterWeak). - subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the shear field, noise-map, etc.)? - -fit_weak: {} # Settings for plots of fits to weak lensing shear catalogues (e.g. PlotterWeak). - -fit_ellipse: # Settings for plots of ellipse fitting fits (e.g. FitEllipse) - data : true # Plot the data of the ellipse fit? - data_no_ellipse: true # Plot the data without the black data ellipses, which obscure noisy data? - -galaxies: # Settings for plots of galaxies (e.g. Galaxies). - subplot_galaxies: true # Plot subplot of all quantities in each galaxies group (e.g. images, convergence)? - subplot_galaxy_images: false # Plot subplot of the image of each galaxy in the model? +# The `plots` section customizes every image that is output to hard-disk during a model-fit. + +# For example, if `plots: fit: subplot_fit=True``, the ``subplot_fit.png`` subplot file will +# be plotted every time visualization is performed. + +# There are two settings which are important for inspecting results via the dataset after a fit is complete which are: + +# - `fits_dataset`: This outputs `dataset.fits` which the database functionality may use to reperform fits. +# -`fits_adapt_images`, This outputs `adapt_images.fits` which the database functionality may use to reperform fits. + +# These can be disabled to save on hard-disk space but will lead to certain database functionality being disabled. + +subplot_format: [png] # Output format of all subplots, can be png, pdf or both (e.g. [png, pdf]) +fits_are_zoomed: false # If true, output .fits files are zoomed in on the center of the unmasked region image, saving hard-disk space. + +dataset: # Settings for plots of all datasets (e.g. Imaging, Interferometer). + subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the data, noise-map, etc.)? + fits_dataset: true # Output a .fits file containing the dataset data, noise-map and other quantities? + +positions: # Settings for plots with resampling image-positions on (e.g. the image). + image_with_positions: true + +fit: # Settings for plots of all fits (e.g. FitImaging, FitInterferometer). + subplot_fit: true # Plot subplot of all fit quantities for any dataset (e.g. the model data, residual-map, etc.)? + subplot_fit_log10: false # Plot subplot of all fit quantities for any dataset using log10 color maps (e.g. the model data, residual-map, etc.)? + subplot_of_planes: false # Plot subplot of the model-image, subtracted image and other quantities of each plane? + subplot_galaxies_images: false # Plot subplot of the image of each plane in the model? + fits_fit: true # Output a .fits file containing the fit model data, residual map, normalized residual map and chi-squared? + fits_galaxy_images : true # Output a .fits file containing the images (e.g. without PSF convolution) of every galaxy? + fits_model_galaxy_images : true # Output a .fits file containing the model images (e.g. with PSF convolution) of every galaxy? + +fit_imaging: {} # Settings for plots of fits to imaging datasets (e.g. FitImaging). + +tracer: # Settings for plots of tracers (e.g. Tracer). + subplot_tracer: true # Plot subplot of all quantities in each tracer (e.g. images, convergence)? + subplot_galaxies_images: false # Plot subplot of the image of each plane in the tracer? + fits_tracer: true # Output tracer.fits file of tracer's convergence, potential, deflections_y and deflections_x? + fits_source_plane_images: true # Output source_plane_images.fits file of the source-plane image (light profiles only) of each galaxy in the tracer? + fits_source_plane_shape: (100, 100) # The shape of the source-plane image output in the fits_source_plane_images.fits file. + +inversion: # Settings for plots of inversions (e.g. InversionPlotter). + subplot_inversion: true # Plot subplot of all quantities in each inversion (e.g. reconstrucuted image, reconstruction)? + subplot_mappings: false # Plot subplot of the image-to-source pixels mappings of each pixelization? + csv_reconstruction: true # output source_plane_reconstruction_0.csv containing the source-plane mesh y, x, reconstruction and noise map values. + +adapt: # Settings for plots of adapt images used by adaptive pixelizations. + subplot_adapt_images: true # Plot subplot showing each adapt image used for adaptive pixelization? + fits_adapt_images: true # Output a .fits file containing the adapt images used for adaptive pixelization? + +fit_interferometer: # Settings for plots of fits to interferometer datasets (e.g. FitInterferometer). + subplot_fit_dirty_images: false # Plot subplot of the dirty-images of all interferometer datasets? + subplot_fit_real_space: false # Plot subplot of the real-space images of all interferometer datasets? + fits_dirty_images: true # output dirty_images.fits showing the dirty image, noise-map, model-data, resiual-map, normalized residual map and chi-squared map? + +point_dataset: # Settings for plots of point source datasets (e.g. PointDatasetPlotter). + subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the data, noise-map, etc.)? + +fit_point_dataset: {} # Settings for plots of fits to point source datasets (e.g. FitPointDatasetPlotter). + +weak_dataset: # Settings for plots of weak lensing shear catalogues (e.g. PlotterWeak). + subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the shear field, noise-map, etc.)? + +fit_weak: {} # Settings for plots of fits to weak lensing shear catalogues (e.g. PlotterWeak). + +fit_ellipse: # Settings for plots of ellipse fitting fits (e.g. FitEllipse) + data : true # Plot the data of the ellipse fit? + data_no_ellipse: true # Plot the data without the black data ellipses, which obscure noisy data? + +galaxies: # Settings for plots of galaxies (e.g. Galaxies). + subplot_galaxies: true # Plot subplot of all quantities in each galaxies group (e.g. images, convergence)? + subplot_galaxy_images: false # Plot subplot of the image of each galaxy in the model? diff --git a/autolens/exc.py b/autolens/exc.py index 69be1d91f..ed739a462 100644 --- a/autolens/exc.py +++ b/autolens/exc.py @@ -1,54 +1,54 @@ -import autofit as af -from autofit.exc import * -from autoarray.exc import * -from autogalaxy.exc import * - - -class RayTracingException(af.exc.FitException): - """ - Raises exceptions associated with the `lens/tracer.py` module and `Tracer` class. - - This exception inherits from a `FitException`. This means that if this exception is raised during a model-fit in - the analysis class's `log_likelihood_function` that model is resampled and does not terminate the code. - """ - - pass - - -class PositionsException(af.exc.FitException): - """ - Raises exceptions associated with the positions data in the `point` module. - - For example if the multiple image positions do not meet certain format requirements. - - This exception inehrits from a `FitException`. This means that if this exception is raised during a model-fit in - the analysis class's `log_likelihood_function` that model is resampled and does not terminate the code. - """ - - pass - - -class PixelizationException(af.exc.FitException): - """ - Raises exceptions associated with the `inversion/pixelization` modules and `Pixelization` classes. - - For example if a `RectangularAdaptDensity` mesh has dimensions below 3x3. - - This exception overwrites `autoarray.exc.PixelizationException` in order to add a `FitException`. This means that - if this exception is raised during a model-fit in the analysis class's `log_likelihood_function` that model - is resampled and does not terminate the code. - """ - - pass - - -class PointExtractionException(Exception): - """ - Raises exceptions associated with the extraction of quantities in the `point` module, where the name of a - `PointSource` profile often relates to a model-component. - - For example if one tries to extract a profile `point_1` but there is no corresponding `PointSource` profile - named `point_1`. - """ - - pass +import autofit as af +from autofit.exc import * +from autoarray.exc import * +from autogalaxy.exc import * + + +class RayTracingException(af.exc.FitException): + """ + Raises exceptions associated with the `lens/tracer.py` module and `Tracer` class. + + This exception inherits from a `FitException`. This means that if this exception is raised during a model-fit in + the analysis class's `log_likelihood_function` that model is resampled and does not terminate the code. + """ + + pass + + +class PositionsException(af.exc.FitException): + """ + Raises exceptions associated with the positions data in the `point` module. + + For example if the multiple image positions do not meet certain format requirements. + + This exception inehrits from a `FitException`. This means that if this exception is raised during a model-fit in + the analysis class's `log_likelihood_function` that model is resampled and does not terminate the code. + """ + + pass + + +class PixelizationException(af.exc.FitException): + """ + Raises exceptions associated with the `inversion/pixelization` modules and `Pixelization` classes. + + For example if a `RectangularAdaptDensity` mesh has dimensions below 3x3. + + This exception overwrites `autoarray.exc.PixelizationException` in order to add a `FitException`. This means that + if this exception is raised during a model-fit in the analysis class's `log_likelihood_function` that model + is resampled and does not terminate the code. + """ + + pass + + +class PointExtractionException(Exception): + """ + Raises exceptions associated with the extraction of quantities in the `point` module, where the name of a + `PointSource` profile often relates to a model-component. + + For example if one tries to extract a profile `point_1` but there is no corresponding `PointSource` profile + named `point_1`. + """ + + pass diff --git a/autolens/fixtures.py b/autolens/fixtures.py index 9df614484..7b13a1a26 100644 --- a/autolens/fixtures.py +++ b/autolens/fixtures.py @@ -1,184 +1,184 @@ -import autolens as al - -from autogalaxy.fixtures import * - - -def make_grid_2d_7x7(): - return aa.Grid2D.from_mask(mask=make_mask_2d_7x7(), over_sample_size=1) - - -def make_positions_x2(): - return al.Grid2DIrregular(values=[(1.0, 1.0), (2.0, 2.0)]) - - -def make_positions_noise_map_x2(): - return al.ArrayIrregular(values=[1.0, 1.0]) - - -def make_fluxes_x2(): - return al.ArrayIrregular(values=[1.0, 2.0]) - - -def make_fluxes_noise_map_x2(): - return al.ArrayIrregular(values=[1.0, 1.0]) - - -def make_point_dataset(): - return al.PointDataset( - name="point_0", - positions=make_positions_x2(), - positions_noise_map=make_positions_noise_map_x2(), - fluxes=make_fluxes_x2(), - fluxes_noise_map=make_fluxes_noise_map_x2(), - ) - - -def make_solver(): - grid = al.Grid2D.uniform(shape_native=(10, 10), pixel_scales=0.5) - return al.PointSolver.for_grid( - grid=grid, pixel_scale_precision=0.25, magnification_threshold=1e-8 - ) - - -def make_tracer_x1_plane_7x7(): - return al.Tracer(galaxies=[make_gal_x1_lp()]) - - -def make_tracer_x2_plane_7x7(): - source_gal_x1_lp = al.Galaxy(redshift=1.0, light_profile_0=make_lp_0()) - - return al.Tracer(galaxies=[make_gal_x1_mp(), make_gal_x1_lp(), source_gal_x1_lp]) - - -def make_tracer_x2_plane_inversion_7x7(): - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(), regularization=al.reg.Constant() - ) - - source_gal_inversion = al.Galaxy(redshift=1.0, pixelization=pixelization) - - return al.Tracer( - galaxies=[make_gal_x1_mp(), make_gal_x1_lp(), source_gal_inversion] - ) - - -def make_tracer_x2_plane_point(): - source_gal_x1_lp = al.Galaxy(redshift=1.0, point_0=al.ps.PointFlux()) - - return al.Tracer(galaxies=[make_gal_x1_mp(), make_gal_x1_lp(), source_gal_x1_lp]) - - -def make_fit_imaging_x1_plane_7x7(): - return al.FitImaging( - dataset=make_masked_imaging_7x7(), tracer=make_tracer_x1_plane_7x7() - ) - - -def make_fit_imaging_x2_plane_7x7(): - return al.FitImaging( - dataset=make_masked_imaging_7x7(), tracer=make_tracer_x2_plane_7x7() - ) - - -def make_fit_imaging_x2_plane_inversion_7x7(): - return al.FitImaging( - dataset=make_masked_imaging_7x7(), tracer=make_tracer_x2_plane_inversion_7x7() - ) - - -def make_fit_interferometer_x1_plane_7x7(): - return al.FitInterferometer( - dataset=make_interferometer_7(), - tracer=make_tracer_x1_plane_7x7(), - ) - - -def make_fit_interferometer_x2_plane_7x7(): - return al.FitInterferometer( - dataset=make_interferometer_7(), - tracer=make_tracer_x2_plane_7x7(), - ) - - -def make_fit_interferometer_x2_plane_inversion_7x7(): - return al.FitInterferometer( - dataset=make_interferometer_7(), - tracer=make_tracer_x2_plane_inversion_7x7(), - ) - - -def make_fit_point_dataset_x2_plane(): - return al.FitPointDataset( - dataset=make_point_dataset(), - tracer=make_tracer_x2_plane_point(), - solver=make_solver(), - ) - - -def make_adapt_galaxy_name_image_dict_7x7(): - image_0 = ag.Array2D( - np.full(fill_value=2.0, shape=make_mask_2d_7x7().pixels_in_mask), - mask=make_mask_2d_7x7(), - ) - - image_1 = ag.Array2D( - np.full(fill_value=3.0, shape=make_mask_2d_7x7().pixels_in_mask), - mask=make_mask_2d_7x7(), - ) - - adapt_galaxy_name_image_dict = { - "('galaxies', 'lens')": image_0, - "('galaxies', 'source')": image_1, - } - - return adapt_galaxy_name_image_dict - - -def make_adapt_galaxy_name_image_plane_mesh_grid_dict_7x7(): - image_plane_mesh_grid_0 = ag.Grid2DIrregular( - values=[(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)] - ) - - image_plane_mesh_grid_1 = ag.Grid2DIrregular( - values=[(3.0, 3.0), (4.0, 4.0), (5.0, 5.0)] - ) - - adapt_galaxy_name_image_plane_mesh_grid_dict = { - str(("galaxies", "lens")): image_plane_mesh_grid_0, - str(("galaxies", "source")): image_plane_mesh_grid_1, - } - - return adapt_galaxy_name_image_plane_mesh_grid_dict - - -def make_adapt_images_7x7(): - return ag.AdaptImages( - galaxy_name_image_dict=make_adapt_galaxy_name_image_dict_7x7(), - galaxy_name_image_plane_mesh_grid_dict=make_adapt_galaxy_name_image_plane_mesh_grid_dict_7x7(), - ) - - -def make_analysis_imaging_7x7(): - analysis = al.AnalysisImaging( - dataset=make_masked_imaging_7x7(), - use_jax=False, - adapt_images=make_adapt_images_7x7(), - ) - return analysis - - -def make_analysis_interferometer_7(): - analysis = al.AnalysisInterferometer( - dataset=make_interferometer_7(), - adapt_images=make_adapt_images_7x7(), - use_jax=False, - ) - return analysis - - -def make_analysis_point_x2(): - return al.AnalysisPoint( - point_dict=make_point_dict(), - solver=al.m.MockPointSolver(model_positions=make_positions_x2()), - use_jax=False, - ) +import autolens as al + +from autogalaxy.fixtures import * + + +def make_grid_2d_7x7(): + return aa.Grid2D.from_mask(mask=make_mask_2d_7x7(), over_sample_size=1) + + +def make_positions_x2(): + return al.Grid2DIrregular(values=[(1.0, 1.0), (2.0, 2.0)]) + + +def make_positions_noise_map_x2(): + return al.ArrayIrregular(values=[1.0, 1.0]) + + +def make_fluxes_x2(): + return al.ArrayIrregular(values=[1.0, 2.0]) + + +def make_fluxes_noise_map_x2(): + return al.ArrayIrregular(values=[1.0, 1.0]) + + +def make_point_dataset(): + return al.PointDataset( + name="point_0", + positions=make_positions_x2(), + positions_noise_map=make_positions_noise_map_x2(), + fluxes=make_fluxes_x2(), + fluxes_noise_map=make_fluxes_noise_map_x2(), + ) + + +def make_solver(): + grid = al.Grid2D.uniform(shape_native=(10, 10), pixel_scales=0.5) + return al.PointSolver.for_grid( + grid=grid, pixel_scale_precision=0.25, magnification_threshold=1e-8 + ) + + +def make_tracer_x1_plane_7x7(): + return al.Tracer(galaxies=[make_gal_x1_lp()]) + + +def make_tracer_x2_plane_7x7(): + source_gal_x1_lp = al.Galaxy(redshift=1.0, light_profile_0=make_lp_0()) + + return al.Tracer(galaxies=[make_gal_x1_mp(), make_gal_x1_lp(), source_gal_x1_lp]) + + +def make_tracer_x2_plane_inversion_7x7(): + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(), regularization=al.reg.Constant() + ) + + source_gal_inversion = al.Galaxy(redshift=1.0, pixelization=pixelization) + + return al.Tracer( + galaxies=[make_gal_x1_mp(), make_gal_x1_lp(), source_gal_inversion] + ) + + +def make_tracer_x2_plane_point(): + source_gal_x1_lp = al.Galaxy(redshift=1.0, point_0=al.ps.PointFlux()) + + return al.Tracer(galaxies=[make_gal_x1_mp(), make_gal_x1_lp(), source_gal_x1_lp]) + + +def make_fit_imaging_x1_plane_7x7(): + return al.FitImaging( + dataset=make_masked_imaging_7x7(), tracer=make_tracer_x1_plane_7x7() + ) + + +def make_fit_imaging_x2_plane_7x7(): + return al.FitImaging( + dataset=make_masked_imaging_7x7(), tracer=make_tracer_x2_plane_7x7() + ) + + +def make_fit_imaging_x2_plane_inversion_7x7(): + return al.FitImaging( + dataset=make_masked_imaging_7x7(), tracer=make_tracer_x2_plane_inversion_7x7() + ) + + +def make_fit_interferometer_x1_plane_7x7(): + return al.FitInterferometer( + dataset=make_interferometer_7(), + tracer=make_tracer_x1_plane_7x7(), + ) + + +def make_fit_interferometer_x2_plane_7x7(): + return al.FitInterferometer( + dataset=make_interferometer_7(), + tracer=make_tracer_x2_plane_7x7(), + ) + + +def make_fit_interferometer_x2_plane_inversion_7x7(): + return al.FitInterferometer( + dataset=make_interferometer_7(), + tracer=make_tracer_x2_plane_inversion_7x7(), + ) + + +def make_fit_point_dataset_x2_plane(): + return al.FitPointDataset( + dataset=make_point_dataset(), + tracer=make_tracer_x2_plane_point(), + solver=make_solver(), + ) + + +def make_adapt_galaxy_name_image_dict_7x7(): + image_0 = ag.Array2D( + np.full(fill_value=2.0, shape=make_mask_2d_7x7().pixels_in_mask), + mask=make_mask_2d_7x7(), + ) + + image_1 = ag.Array2D( + np.full(fill_value=3.0, shape=make_mask_2d_7x7().pixels_in_mask), + mask=make_mask_2d_7x7(), + ) + + adapt_galaxy_name_image_dict = { + "('galaxies', 'lens')": image_0, + "('galaxies', 'source')": image_1, + } + + return adapt_galaxy_name_image_dict + + +def make_adapt_galaxy_name_image_plane_mesh_grid_dict_7x7(): + image_plane_mesh_grid_0 = ag.Grid2DIrregular( + values=[(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)] + ) + + image_plane_mesh_grid_1 = ag.Grid2DIrregular( + values=[(3.0, 3.0), (4.0, 4.0), (5.0, 5.0)] + ) + + adapt_galaxy_name_image_plane_mesh_grid_dict = { + str(("galaxies", "lens")): image_plane_mesh_grid_0, + str(("galaxies", "source")): image_plane_mesh_grid_1, + } + + return adapt_galaxy_name_image_plane_mesh_grid_dict + + +def make_adapt_images_7x7(): + return ag.AdaptImages( + galaxy_name_image_dict=make_adapt_galaxy_name_image_dict_7x7(), + galaxy_name_image_plane_mesh_grid_dict=make_adapt_galaxy_name_image_plane_mesh_grid_dict_7x7(), + ) + + +def make_analysis_imaging_7x7(): + analysis = al.AnalysisImaging( + dataset=make_masked_imaging_7x7(), + use_jax=False, + adapt_images=make_adapt_images_7x7(), + ) + return analysis + + +def make_analysis_interferometer_7(): + analysis = al.AnalysisInterferometer( + dataset=make_interferometer_7(), + adapt_images=make_adapt_images_7x7(), + use_jax=False, + ) + return analysis + + +def make_analysis_point_x2(): + return al.AnalysisPoint( + point_dict=make_point_dict(), + solver=al.m.MockPointSolver(model_positions=make_positions_x2()), + use_jax=False, + ) diff --git a/autolens/imaging/fit_imaging.py b/autolens/imaging/fit_imaging.py index 5c5703208..8da5ebfd1 100644 --- a/autolens/imaging/fit_imaging.py +++ b/autolens/imaging/fit_imaging.py @@ -1,401 +1,401 @@ -""" -Imaging fit class for strong gravitational lens modeling. - -``FitImaging`` extends the ``autogalaxy`` ``FitImaging`` base class to work with a -``Tracer`` instead of a plain ``Galaxies`` collection. The fit pipeline is the same -six-step process as the base class, with the critical addition that light profiles from -source galaxies are evaluated *after* ray-tracing their image-plane grid through the -lens mass distribution: - -1. Evaluate all light profiles of the tracer galaxies on the (ray-traced) grid. -2. Blur the summed image with the imaging PSF. -3. Subtract the blurred image from the data to form the profile-subtracted image. -4. If the tracer contains linear light profiles or pixelizations, solve for their - amplitudes / reconstructed source via an inversion of the profile-subtracted image. -5. Combine blurred image and inversion reconstruction into the ``model_data``. -6. Compute residuals, chi-squared, and log likelihood (or log evidence when an inversion - is present). - -The ``TracerToInversion`` helper is used to assemble the linear system in step 4. -""" -import copy -import functools -import numpy as np -from typing import Dict, List, Optional - -from autonerves import cached_property - -import autoarray as aa -import autogalaxy as ag - -from autogalaxy.abstract_fit import AbstractFitInversion - -from autolens.lens.tracer import Tracer -from autolens.lens.to_inversion import TracerToInversion - -from autolens import exc - - -class FitImaging(aa.FitImaging, AbstractFitInversion): - def __init__( - self, - dataset: aa.Imaging, - tracer: Tracer, - dataset_model : Optional[aa.DatasetModel] = None, - adapt_images: Optional[ag.AdaptImages] = None, - settings: aa.Settings = None, - xp=np, - preloads=None, - ): - """ - Fits an imaging dataset using a `Tracer` object. - - The fit performs the following steps: - - 1) Compute the sum of all images of galaxy light profiles in the `Tracer`. - - 2) Blur this with the imaging PSF to created the `blurred_image`. - - 3) Subtract this image from the `data` to create the `profile_subtracted_image`. - - 4) If the `Tracer` has any linear algebra objects (e.g. linear light profiles, a pixelization / regulariation) - fit the `profile_subtracted_image` with these objects via an inversion. - - 5) Compute the `model_data` as the sum of the `blurred_image` and `reconstructed_data` of the inversion (if - an inversion is not performed the `model_data` is only the `blurred_image`. - - 6) Subtract the `model_data` from the data and compute the residuals, chi-squared and likelihood via the - noise-map (if an inversion is performed the `log_evidence`, including additional terms describing the linear - algebra solution, is computed). - - When performing a `model-fit`via an `AnalysisImaging` object the `figure_of_merit` of this object - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - dataset - The imaging dataset which is fitted by the galaxies in the tracer. - tracer - The tracer of galaxies whose light profile images are used to fit the imaging data. - dataset_model - Attributes which allow for parts of a dataset to be treated as a model (e.g. the background sky level). - adapt_images - Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the - reconstructed galaxy's morphology. - settings - Settings controlling how an inversion is fitted for example which linear algebra formalism is used. - preloads - An optional `PreloadsImaging` carrying exposure-invariant quantities (the shared - source-plane mesh geometry) computed once and reused by the fit instead of being - rebuilt. Supplied by the multi-exposure shared-state path (see - `AnalysisImaging.shared_state_from`); `None` (the default) fits as normal. - """ - - super().__init__(dataset=dataset, dataset_model=dataset_model, xp=xp) - AbstractFitInversion.__init__( - self=self, model_obj=tracer, settings=settings, xp=xp - ) - - self.tracer = tracer - - self.adapt_images = adapt_images - self.settings = settings or aa.Settings() - self.preloads = preloads - - @functools.cached_property - def blurred_image(self) -> aa.Array2D: - """ - Returns the image of all light profiles in the fit's tracer convolved with the imaging dataset's PSF. - """ - - if len(self.tracer.cls_list_from(cls=ag.LightProfile)) == len( - self.tracer.cls_list_from(cls=ag.lp_operated.LightProfileOperated) - ): - return self.tracer.image_2d_from( - grid=self.grids.lp, - xp=self._xp, - ) - - return self.tracer.blurred_image_2d_from( - grid=self.grids.lp, - psf=self.dataset.psf, - blurring_grid=self.grids.blurring, - xp=self._xp, - ) - - @functools.cached_property - def profile_subtracted_image(self) -> aa.Array2D: - """ - Returns the dataset's image with all blurred light profile images in the fit's tracer subtracted. - """ - return self.data - self.blurred_image - - @property - def _preloads_scoped(self): - """ - The preloads as consumed by this fit's inversion, scoped to its dataset type. - - Cross-dataset-type shared state (e.g. an interferometer lead factor in a joint - imaging + interferometer graph) may carry a mapper / curvature matrix that embed the - OTHER dataset's grids — consuming them here would silently corrupt the fit. Only the - source-plane mesh geometry is valid across dataset types, so any non-imaging preloads - are reduced to their mesh-geometry view. - """ - if self.preloads is None or isinstance(self.preloads, aa.PreloadsImaging): - return self.preloads - - return aa.PreloadsImaging( - source_plane_mesh_grid=self.preloads.source_plane_mesh_grid, - image_plane_mesh_grid=self.preloads.image_plane_mesh_grid, - ) - - @property - def tracer_to_inversion(self) -> TracerToInversion: - - dataset = aa.DatasetInterface( - data=self.profile_subtracted_image, - noise_map=self.noise_map, - grids=self.grids, - psf=self.dataset.psf, - sparse_operator=self.dataset.sparse_operator, - ) - - return TracerToInversion( - dataset=dataset, - tracer=self.tracer, - adapt_images=self.adapt_images, - settings=self.settings, - xp=self._xp, - preloads=self._preloads_scoped, - ) - - @cached_property - def inversion(self) -> Optional[aa.AbstractInversion]: - """ - If the tracer has linear objects which are used to fit the data (e.g. a linear light profile / pixelization) - this function returns a linear inversion, where the flux values of these objects (e.g. the `intensity` - of linear light profiles) are computed via linear matrix algebra. - - The data passed to this function is the dataset's image with all light profile images of the tracer subtracted, - ensuring that the inversion only fits the data with ordinary light profiles subtracted. - """ - if self.perform_inversion: - - return self.tracer_to_inversion.inversion - - @functools.cached_property - def model_data(self) -> aa.Array2D: - """ - Returns the model-image that is used to fit the data. - - If the tracer does not have any linear objects and therefore omits an inversion, the model data is the - sum of all light profile images blurred with the PSF. - - If a inversion is included it is the sum of this image and the inversion's reconstruction of the image. - """ - - if self.perform_inversion: - - return self.blurred_image + self.inversion.mapped_reconstructed_operated_data - - return self.blurred_image - - @property - def galaxy_image_dict(self) -> Dict[ag.Galaxy, np.ndarray]: - """ - A dictionary which associates every galaxy in the tracer before operation (e.g. no PSF convolution - or NUFFT performed). - - This image is the image of the sum of: - - - The images of all ordinary light profiles in that plane summed before any operation is performed on them. - - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved - for first via the inversion. - - This dictionary is used to output to .fits file the galaxy images. - """ - - galaxy_image_2d_dict = self.tracer.galaxy_image_2d_dict_from( - grid=self.grids.lp, - xp=self._xp - ) - - galaxy_linear_obj_image_dict = self.galaxy_linear_obj_data_dict_from( - use_operated=False, - ) - - return {**galaxy_image_2d_dict, **galaxy_linear_obj_image_dict} - - @cached_property - def galaxy_model_image_dict(self) -> Dict[ag.Galaxy, np.ndarray]: - """ - A dictionary which associates every galaxy in the tracer with its `model_image`. - - This image is the image of the sum of: - - - The images of all ordinary light profiles in that plane summed and convolved with the imaging data's PSF. - - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved - for first via the inversion. - - For modeling, this dictionary is used to set up the `adapt_images` that adaptmodel_images_of_planes_list - certain pixelizations to the data being fitted. - """ - - galaxy_blurred_image_2d_dict = self.tracer.galaxy_blurred_image_2d_dict_from( - grid=self.grids.lp, - psf=self.dataset.psf, - blurring_grid=self.grids.blurring, - xp=self._xp - ) - - galaxy_linear_obj_image_dict = self.galaxy_linear_obj_data_dict_from( - use_operated=True, - ) - - return {**galaxy_blurred_image_2d_dict, **galaxy_linear_obj_image_dict} - - @functools.cached_property - def subtracted_images_of_galaxies_dict(self) -> Dict[ag.Galaxy, np.ndarray]: - """ - A dictionary which associates every galaxy in the tracer with its `subtracted image`. - - A subtracted image of a galaxy is the data where all other galaxy images are subtracted from it, therefore - showing how a galaxy appears in the data in the absence of all other galaxies. - - This is used to visualize the contribution of each galaxy in the data. - """ - - subtracted_images_of_galaxies_dict = {} - - for (galaxy, galaxy_model_image) in self.galaxy_model_image_dict.items(): - subtracted_images_of_galaxies_dict[galaxy] = copy.copy(self.dataset.data) - - for (galaxy, galaxy_model_image) in self.galaxy_model_image_dict.items(): - for (galaxy_other, galaxy_model_image_other) in self.galaxy_model_image_dict.items(): - if galaxy != galaxy_other: - subtracted_images_of_galaxies_dict[galaxy] -= galaxy_model_image_other - - return subtracted_images_of_galaxies_dict - - @functools.cached_property - def subtracted_signal_to_noise_maps_of_galaxies_dict(self) -> Dict[ag.Galaxy, np.ndarray]: - """ - A dictionary which associates every galaxy in the tracer with its `subtracted image`. - - A subtracted image of a galaxy is the data where all other galaxy images are subtracted from it, therefore - showing how a galaxy appears in the data in the absence of all other galaxies. - - This is used to visualize the contribution of each galaxy in the data. - """ - - subtracted_signal_to_noise_maps_of_galaxies_dict = {} - - for (galaxy, subtracted_image) in self.subtracted_images_of_galaxies_dict.items(): - - subtracted_signal_to_noise_maps_of_galaxies_dict[galaxy] = subtracted_image / self.noise_map - - return subtracted_signal_to_noise_maps_of_galaxies_dict - - @cached_property - def model_images_of_planes_list(self) -> List[aa.Array2D]: - """ - A list of every model image of every plane in the tracer. - - This image is the image of the sum of: - - - The images of all ordinary light profiles in that plane summed and convolved with the imaging data's PSF. - - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved - for first via the inversion. - - This is used to visualize the different contibutions of light from the image-plane, source-plane and other - planes in a fit. - """ - galaxy_model_image_dict = self.galaxy_model_image_dict - - model_images_of_planes_list = [ - aa.Array2D( - values=np.zeros(self.grids.lp.shape_slim), mask=self.dataset.mask - ) - for i in range(self.tracer.total_planes) - ] - - for plane_index, galaxies in enumerate(self.tracer.planes): - for galaxy in galaxies: - model_images_of_planes_list[plane_index] += galaxy_model_image_dict[ - galaxy - ] - - return model_images_of_planes_list - - @functools.cached_property - def subtracted_images_of_planes_list(self) -> List[aa.Array2D]: - """ - A list of the subtracted image of every plane. - - A subtracted image of a plane is the data where all other plane images are subtracted from it, therefore - showing how a plane appears in the data in the absence of all other planes. - - This is used to visualize the contribution of each plane in the data. - """ - - # TODO: Check why this gives weird results via aggregator. - - subtracted_images_of_planes_list = [] - - model_images_of_planes_list = self.model_images_of_planes_list - - for galaxy_index in range(len(self.tracer.planes)): - - other_planes_model_images = [ - model_image - for i, model_image in enumerate(model_images_of_planes_list) - if i != galaxy_index - ] - - subtracted_image = self.data - sum(other_planes_model_images) - - subtracted_images_of_planes_list.append(subtracted_image) - - return subtracted_images_of_planes_list - - @property - def unmasked_blurred_image(self) -> aa.Array2D: - """ - The blurred image of the overall fit that would be evaluated without a mask being used. - - Linear objects are tied to the mask defined to used to perform the fit, therefore their unmasked blurred - image cannot be computed. - """ - if self.tracer.has(cls=ag.lp_linear.LightProfileLinear): - exc.raise_linear_light_profile_in_unmasked() - - return self.tracer.unmasked_blurred_image_2d_from( - grid=self.grids.lp, psf=self.dataset.psf - ) - - @property - def unmasked_blurred_image_of_planes_list(self) -> List[aa.Array2D]: - """ - The blurred image of every galaxy in the tracer used in this fit, that would be evaluated without a mask being - used. - - Linear objects are tied to the mask defined to used to perform the fit, therefore their unmasked blurred - image cannot be computed. - """ - if self.tracer.has(cls=ag.lp_linear.LightProfileLinear): - exc.raise_linear_light_profile_in_unmasked() - - return self.tracer.unmasked_blurred_image_2d_list_from( - grid=self.grids.lp, psf=self.dataset.psf - ) - - @property - def tracer_linear_light_profiles_to_light_profiles(self) -> Tracer: - """ - The `Tracer` where all linear light profiles have been converted to ordinary light profiles, where their - `intensity` values are set to the values inferred by this fit. - - This is typically used for visualization, because linear light profiles cannot be used in `LightProfile` - or `Galaxy` objects. - """ - return self.model_obj_linear_light_profiles_to_light_profiles +""" +Imaging fit class for strong gravitational lens modeling. + +``FitImaging`` extends the ``autogalaxy`` ``FitImaging`` base class to work with a +``Tracer`` instead of a plain ``Galaxies`` collection. The fit pipeline is the same +six-step process as the base class, with the critical addition that light profiles from +source galaxies are evaluated *after* ray-tracing their image-plane grid through the +lens mass distribution: + +1. Evaluate all light profiles of the tracer galaxies on the (ray-traced) grid. +2. Blur the summed image with the imaging PSF. +3. Subtract the blurred image from the data to form the profile-subtracted image. +4. If the tracer contains linear light profiles or pixelizations, solve for their + amplitudes / reconstructed source via an inversion of the profile-subtracted image. +5. Combine blurred image and inversion reconstruction into the ``model_data``. +6. Compute residuals, chi-squared, and log likelihood (or log evidence when an inversion + is present). + +The ``TracerToInversion`` helper is used to assemble the linear system in step 4. +""" +import copy +import functools +import numpy as np +from typing import Dict, List, Optional + +from autonerves import cached_property + +import autoarray as aa +import autogalaxy as ag + +from autogalaxy.abstract_fit import AbstractFitInversion + +from autolens.lens.tracer import Tracer +from autolens.lens.to_inversion import TracerToInversion + +from autolens import exc + + +class FitImaging(aa.FitImaging, AbstractFitInversion): + def __init__( + self, + dataset: aa.Imaging, + tracer: Tracer, + dataset_model : Optional[aa.DatasetModel] = None, + adapt_images: Optional[ag.AdaptImages] = None, + settings: aa.Settings = None, + xp=np, + preloads=None, + ): + """ + Fits an imaging dataset using a `Tracer` object. + + The fit performs the following steps: + + 1) Compute the sum of all images of galaxy light profiles in the `Tracer`. + + 2) Blur this with the imaging PSF to created the `blurred_image`. + + 3) Subtract this image from the `data` to create the `profile_subtracted_image`. + + 4) If the `Tracer` has any linear algebra objects (e.g. linear light profiles, a pixelization / regulariation) + fit the `profile_subtracted_image` with these objects via an inversion. + + 5) Compute the `model_data` as the sum of the `blurred_image` and `reconstructed_data` of the inversion (if + an inversion is not performed the `model_data` is only the `blurred_image`. + + 6) Subtract the `model_data` from the data and compute the residuals, chi-squared and likelihood via the + noise-map (if an inversion is performed the `log_evidence`, including additional terms describing the linear + algebra solution, is computed). + + When performing a `model-fit`via an `AnalysisImaging` object the `figure_of_merit` of this object + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + dataset + The imaging dataset which is fitted by the galaxies in the tracer. + tracer + The tracer of galaxies whose light profile images are used to fit the imaging data. + dataset_model + Attributes which allow for parts of a dataset to be treated as a model (e.g. the background sky level). + adapt_images + Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the + reconstructed galaxy's morphology. + settings + Settings controlling how an inversion is fitted for example which linear algebra formalism is used. + preloads + An optional `PreloadsImaging` carrying exposure-invariant quantities (the shared + source-plane mesh geometry) computed once and reused by the fit instead of being + rebuilt. Supplied by the multi-exposure shared-state path (see + `AnalysisImaging.shared_state_from`); `None` (the default) fits as normal. + """ + + super().__init__(dataset=dataset, dataset_model=dataset_model, xp=xp) + AbstractFitInversion.__init__( + self=self, model_obj=tracer, settings=settings, xp=xp + ) + + self.tracer = tracer + + self.adapt_images = adapt_images + self.settings = settings or aa.Settings() + self.preloads = preloads + + @functools.cached_property + def blurred_image(self) -> aa.Array2D: + """ + Returns the image of all light profiles in the fit's tracer convolved with the imaging dataset's PSF. + """ + + if len(self.tracer.cls_list_from(cls=ag.LightProfile)) == len( + self.tracer.cls_list_from(cls=ag.lp_operated.LightProfileOperated) + ): + return self.tracer.image_2d_from( + grid=self.grids.lp, + xp=self._xp, + ) + + return self.tracer.blurred_image_2d_from( + grid=self.grids.lp, + psf=self.dataset.psf, + blurring_grid=self.grids.blurring, + xp=self._xp, + ) + + @functools.cached_property + def profile_subtracted_image(self) -> aa.Array2D: + """ + Returns the dataset's image with all blurred light profile images in the fit's tracer subtracted. + """ + return self.data - self.blurred_image + + @property + def _preloads_scoped(self): + """ + The preloads as consumed by this fit's inversion, scoped to its dataset type. + + Cross-dataset-type shared state (e.g. an interferometer lead factor in a joint + imaging + interferometer graph) may carry a mapper / curvature matrix that embed the + OTHER dataset's grids — consuming them here would silently corrupt the fit. Only the + source-plane mesh geometry is valid across dataset types, so any non-imaging preloads + are reduced to their mesh-geometry view. + """ + if self.preloads is None or isinstance(self.preloads, aa.PreloadsImaging): + return self.preloads + + return aa.PreloadsImaging( + source_plane_mesh_grid=self.preloads.source_plane_mesh_grid, + image_plane_mesh_grid=self.preloads.image_plane_mesh_grid, + ) + + @property + def tracer_to_inversion(self) -> TracerToInversion: + + dataset = aa.DatasetInterface( + data=self.profile_subtracted_image, + noise_map=self.noise_map, + grids=self.grids, + psf=self.dataset.psf, + sparse_operator=self.dataset.sparse_operator, + ) + + return TracerToInversion( + dataset=dataset, + tracer=self.tracer, + adapt_images=self.adapt_images, + settings=self.settings, + xp=self._xp, + preloads=self._preloads_scoped, + ) + + @cached_property + def inversion(self) -> Optional[aa.AbstractInversion]: + """ + If the tracer has linear objects which are used to fit the data (e.g. a linear light profile / pixelization) + this function returns a linear inversion, where the flux values of these objects (e.g. the `intensity` + of linear light profiles) are computed via linear matrix algebra. + + The data passed to this function is the dataset's image with all light profile images of the tracer subtracted, + ensuring that the inversion only fits the data with ordinary light profiles subtracted. + """ + if self.perform_inversion: + + return self.tracer_to_inversion.inversion + + @functools.cached_property + def model_data(self) -> aa.Array2D: + """ + Returns the model-image that is used to fit the data. + + If the tracer does not have any linear objects and therefore omits an inversion, the model data is the + sum of all light profile images blurred with the PSF. + + If a inversion is included it is the sum of this image and the inversion's reconstruction of the image. + """ + + if self.perform_inversion: + + return self.blurred_image + self.inversion.mapped_reconstructed_operated_data + + return self.blurred_image + + @property + def galaxy_image_dict(self) -> Dict[ag.Galaxy, np.ndarray]: + """ + A dictionary which associates every galaxy in the tracer before operation (e.g. no PSF convolution + or NUFFT performed). + + This image is the image of the sum of: + + - The images of all ordinary light profiles in that plane summed before any operation is performed on them. + - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved + for first via the inversion. + + This dictionary is used to output to .fits file the galaxy images. + """ + + galaxy_image_2d_dict = self.tracer.galaxy_image_2d_dict_from( + grid=self.grids.lp, + xp=self._xp + ) + + galaxy_linear_obj_image_dict = self.galaxy_linear_obj_data_dict_from( + use_operated=False, + ) + + return {**galaxy_image_2d_dict, **galaxy_linear_obj_image_dict} + + @cached_property + def galaxy_model_image_dict(self) -> Dict[ag.Galaxy, np.ndarray]: + """ + A dictionary which associates every galaxy in the tracer with its `model_image`. + + This image is the image of the sum of: + + - The images of all ordinary light profiles in that plane summed and convolved with the imaging data's PSF. + - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved + for first via the inversion. + + For modeling, this dictionary is used to set up the `adapt_images` that adaptmodel_images_of_planes_list + certain pixelizations to the data being fitted. + """ + + galaxy_blurred_image_2d_dict = self.tracer.galaxy_blurred_image_2d_dict_from( + grid=self.grids.lp, + psf=self.dataset.psf, + blurring_grid=self.grids.blurring, + xp=self._xp + ) + + galaxy_linear_obj_image_dict = self.galaxy_linear_obj_data_dict_from( + use_operated=True, + ) + + return {**galaxy_blurred_image_2d_dict, **galaxy_linear_obj_image_dict} + + @functools.cached_property + def subtracted_images_of_galaxies_dict(self) -> Dict[ag.Galaxy, np.ndarray]: + """ + A dictionary which associates every galaxy in the tracer with its `subtracted image`. + + A subtracted image of a galaxy is the data where all other galaxy images are subtracted from it, therefore + showing how a galaxy appears in the data in the absence of all other galaxies. + + This is used to visualize the contribution of each galaxy in the data. + """ + + subtracted_images_of_galaxies_dict = {} + + for (galaxy, galaxy_model_image) in self.galaxy_model_image_dict.items(): + subtracted_images_of_galaxies_dict[galaxy] = copy.copy(self.dataset.data) + + for (galaxy, galaxy_model_image) in self.galaxy_model_image_dict.items(): + for (galaxy_other, galaxy_model_image_other) in self.galaxy_model_image_dict.items(): + if galaxy != galaxy_other: + subtracted_images_of_galaxies_dict[galaxy] -= galaxy_model_image_other + + return subtracted_images_of_galaxies_dict + + @functools.cached_property + def subtracted_signal_to_noise_maps_of_galaxies_dict(self) -> Dict[ag.Galaxy, np.ndarray]: + """ + A dictionary which associates every galaxy in the tracer with its `subtracted image`. + + A subtracted image of a galaxy is the data where all other galaxy images are subtracted from it, therefore + showing how a galaxy appears in the data in the absence of all other galaxies. + + This is used to visualize the contribution of each galaxy in the data. + """ + + subtracted_signal_to_noise_maps_of_galaxies_dict = {} + + for (galaxy, subtracted_image) in self.subtracted_images_of_galaxies_dict.items(): + + subtracted_signal_to_noise_maps_of_galaxies_dict[galaxy] = subtracted_image / self.noise_map + + return subtracted_signal_to_noise_maps_of_galaxies_dict + + @cached_property + def model_images_of_planes_list(self) -> List[aa.Array2D]: + """ + A list of every model image of every plane in the tracer. + + This image is the image of the sum of: + + - The images of all ordinary light profiles in that plane summed and convolved with the imaging data's PSF. + - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved + for first via the inversion. + + This is used to visualize the different contibutions of light from the image-plane, source-plane and other + planes in a fit. + """ + galaxy_model_image_dict = self.galaxy_model_image_dict + + model_images_of_planes_list = [ + aa.Array2D( + values=np.zeros(self.grids.lp.shape_slim), mask=self.dataset.mask + ) + for i in range(self.tracer.total_planes) + ] + + for plane_index, galaxies in enumerate(self.tracer.planes): + for galaxy in galaxies: + model_images_of_planes_list[plane_index] += galaxy_model_image_dict[ + galaxy + ] + + return model_images_of_planes_list + + @functools.cached_property + def subtracted_images_of_planes_list(self) -> List[aa.Array2D]: + """ + A list of the subtracted image of every plane. + + A subtracted image of a plane is the data where all other plane images are subtracted from it, therefore + showing how a plane appears in the data in the absence of all other planes. + + This is used to visualize the contribution of each plane in the data. + """ + + # TODO: Check why this gives weird results via aggregator. + + subtracted_images_of_planes_list = [] + + model_images_of_planes_list = self.model_images_of_planes_list + + for galaxy_index in range(len(self.tracer.planes)): + + other_planes_model_images = [ + model_image + for i, model_image in enumerate(model_images_of_planes_list) + if i != galaxy_index + ] + + subtracted_image = self.data - sum(other_planes_model_images) + + subtracted_images_of_planes_list.append(subtracted_image) + + return subtracted_images_of_planes_list + + @property + def unmasked_blurred_image(self) -> aa.Array2D: + """ + The blurred image of the overall fit that would be evaluated without a mask being used. + + Linear objects are tied to the mask defined to used to perform the fit, therefore their unmasked blurred + image cannot be computed. + """ + if self.tracer.has(cls=ag.lp_linear.LightProfileLinear): + exc.raise_linear_light_profile_in_unmasked() + + return self.tracer.unmasked_blurred_image_2d_from( + grid=self.grids.lp, psf=self.dataset.psf + ) + + @property + def unmasked_blurred_image_of_planes_list(self) -> List[aa.Array2D]: + """ + The blurred image of every galaxy in the tracer used in this fit, that would be evaluated without a mask being + used. + + Linear objects are tied to the mask defined to used to perform the fit, therefore their unmasked blurred + image cannot be computed. + """ + if self.tracer.has(cls=ag.lp_linear.LightProfileLinear): + exc.raise_linear_light_profile_in_unmasked() + + return self.tracer.unmasked_blurred_image_2d_list_from( + grid=self.grids.lp, psf=self.dataset.psf + ) + + @property + def tracer_linear_light_profiles_to_light_profiles(self) -> Tracer: + """ + The `Tracer` where all linear light profiles have been converted to ordinary light profiles, where their + `intensity` values are set to the values inferred by this fit. + + This is typically used for visualization, because linear light profiles cannot be used in `LightProfile` + or `Galaxy` objects. + """ + return self.model_obj_linear_light_profiles_to_light_profiles diff --git a/autolens/imaging/mock/mock_fit_imaging.py b/autolens/imaging/mock/mock_fit_imaging.py index d8477d3fb..aeca078ca 100644 --- a/autolens/imaging/mock/mock_fit_imaging.py +++ b/autolens/imaging/mock/mock_fit_imaging.py @@ -1,51 +1,51 @@ -import autoarray as aa - -from autolens.lens.mock.mock_to_inversion import MockTracerToInversion - - -class MockFitImaging(aa.m.MockFitImaging): - def __init__( - self, - tracer=None, - dataset=None, - inversion=None, - noise_map=None, - grid=None, - blurred_image=None, - ): - - dataset = dataset or aa.m.MockDataset() - - super().__init__( - dataset=dataset, - inversion=inversion, - noise_map=noise_map, - blurred_image=blurred_image, - ) - - self._grid = grid - self.tracer = tracer - - @property - def grid(self): - - if self._grid is not None: - return self._grid - - return super().grids.lp - - @property - def grids(self) -> aa.GridsInterface: - - return aa.GridsInterface( - lp=self.grid, - pixelization=self.grid, - ) - - @property - def tracer_to_inversion(self) -> MockTracerToInversion: - - return MockTracerToInversion( - tracer=self.tracer, - image_plane_mesh_grid_pg_list=self.tracer.image_plane_mesh_grid_pg_list, - ) +import autoarray as aa + +from autolens.lens.mock.mock_to_inversion import MockTracerToInversion + + +class MockFitImaging(aa.m.MockFitImaging): + def __init__( + self, + tracer=None, + dataset=None, + inversion=None, + noise_map=None, + grid=None, + blurred_image=None, + ): + + dataset = dataset or aa.m.MockDataset() + + super().__init__( + dataset=dataset, + inversion=inversion, + noise_map=noise_map, + blurred_image=blurred_image, + ) + + self._grid = grid + self.tracer = tracer + + @property + def grid(self): + + if self._grid is not None: + return self._grid + + return super().grids.lp + + @property + def grids(self) -> aa.GridsInterface: + + return aa.GridsInterface( + lp=self.grid, + pixelization=self.grid, + ) + + @property + def tracer_to_inversion(self) -> MockTracerToInversion: + + return MockTracerToInversion( + tracer=self.tracer, + image_plane_mesh_grid_pg_list=self.tracer.image_plane_mesh_grid_pg_list, + ) diff --git a/autolens/imaging/model/analysis.py b/autolens/imaging/model/analysis.py index 88c28c263..9bd4654db 100644 --- a/autolens/imaging/model/analysis.py +++ b/autolens/imaging/model/analysis.py @@ -1,328 +1,328 @@ -""" -Analysis class for fitting a ``Tracer`` lens model to an imaging dataset. - -``AnalysisImaging`` implements the ``log_likelihood_function`` that a ``PyAutoFit`` -non-linear search calls on each iteration. It: - -1. Constructs a ``Tracer`` from the current model instance. -2. Optionally applies adaptive galaxy images to linear components. -3. Calls ``FitImaging`` to evaluate the log likelihood. -4. Returns the figure of merit (log likelihood or log evidence). - -It also manages result output (``ResultImaging``), on-the-fly visualisation -(``VisualizerImaging``), and position-based priors via ``PositionLikelihood``. -""" -import logging - -import autoarray as aa -import autofit as af -import autogalaxy as ag - -from autonerves.fitsable import hdu_list_for_output_from - -from autolens.analysis.analysis.dataset import AnalysisDataset -from autolens.analysis.exceptions import raise_fit_exception -from autolens.analysis.latent import LatentLens -from autolens.imaging.model.result import ResultImaging -from autolens.imaging.model.visualizer import VisualizerImaging -from autolens.imaging.fit_imaging import FitImaging - -logger = logging.getLogger(__name__) - -logger.setLevel(level="INFO") - - -_FIT_IMAGING_PYTREES_REGISTERED = False - - -class AnalysisImaging(AnalysisDataset): - - Result = ResultImaging - Visualizer = VisualizerImaging - Latent = LatentLens - - def __init__( - self, - dataset, - positions_likelihood_list=None, - adapt_images: ag.AdaptImages = None, - cosmology: ag.cosmo.LensingCosmology = None, - settings=None, - raise_inversion_positions_likelihood_exception: bool = True, - title_prefix: str = None, - use_jax: bool = True, - shared_preloads: bool = False, - **kwargs, - ): - """ - Fits a lens model to an imaging dataset via a non-linear search (see `AnalysisDataset` for - the full docstring of the shared parameters). - - Parameters - ---------- - shared_preloads - Opts this analysis into the cross-factor shared-state mechanism of a `FactorGraphModel` - (see `shared_state_from`). Set this to `True` only when this analysis is one of many - exposures of the same lens (e.g. multi-exposure imaging with per-exposure pixel offsets) - sharing an identical lens model, so the exposure-invariant source-plane mesh geometry - can be computed once and reused by every exposure. `False` by default, leaving the - standard per-analysis behaviour unchanged. - """ - super().__init__( - dataset=dataset, - positions_likelihood_list=positions_likelihood_list, - adapt_images=adapt_images, - cosmology=cosmology, - settings=settings, - raise_inversion_positions_likelihood_exception=raise_inversion_positions_likelihood_exception, - title_prefix=title_prefix, - use_jax=use_jax, - **kwargs, - ) - - self.shared_preloads = shared_preloads - - def log_likelihood_function(self, instance: af.ModelInstance, shared=None) -> float: - """ - Given an instance of the model, where the model parameters are set via a non-linear search, fit the model - instance to the imaging dataset. - - This function returns a log likelihood which is used by the non-linear search to guide the model-fit. - - For this analysis class, this function performs the following steps: - - 1) If the analysis has a adapt image, associated the model galaxy images of this dataset to the galaxies in - the model instance. - - 2) Extract attributes which model aspects of the data reductions, like the scaling the background sky - and background noise. - - 3) Extracts all galaxies from the model instance and set up a `Tracer`, which includes ordering the galaxies - by redshift to set up each `Plane`. - - 4) Use the `Tracer` and other attributes to create a `FitImaging` object, which performs steps such as creating - model images of every galaxy in the tracer, blurring them with the imaging dataset's PSF and computing - residuals, a chi-squared statistic and the log likelihood. - - Certain models will fail to fit the dataset and raise an exception. For example if an `Inversion` is used, the - linear algebra calculation may be invalid and raise an Exception. In such circumstances the model is discarded - and its likelihood value is passed to the non-linear search in a way that it ignores it (for example, using a - value of -1.0e99). - - Parameters - ---------- - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - shared - The cross-factor shared state of a `FactorGraphModel`, computed once per evaluation by the lead - factor's `shared_state_from` (see that method). For this analysis it is a `PreloadsImaging` - carrying the exposure-invariant source-plane mesh geometry; when provided it is reused by the fit - instead of being recomputed. `None` (the default, e.g. a standalone fit) leaves behaviour unchanged. - - Returns - ------- - float - The log likelihood indicating how well this model instance fitted the imaging data. - """ - - log_likelihood_penalty = self.log_likelihood_penalty_from( - instance=instance, - ) - - if self._use_jax: - return ( - self.fit_from(instance=instance, preloads=shared).figure_of_merit - - log_likelihood_penalty - ) - - try: - return ( - self.fit_from(instance=instance, preloads=shared).figure_of_merit - - log_likelihood_penalty - ) - except Exception as e: - raise_fit_exception(e) - - def shared_state_from(self, instance: af.ModelInstance): - """ - Compute the exposure-invariant source-plane mesh geometry once so it can be shared across the factors - of a multi-exposure `FactorGraphModel` (see `autofit.Analysis.shared_state_from`). - - When `shared_preloads` is set, every factor of the graph is an exposure of the same lens sharing an - identical lens model, so the source-plane mesh (the image-mesh centres of this lead exposure, - ray-traced through the shared lens model) is built once here and returned inside a `PreloadsImaging`, - which `FactorGraphModel` forwards as the `shared` argument to every factor's - `log_likelihood_function`. Each exposure then maps its own (offset) data grid onto the shared mesh - instead of computing its own image-mesh and mesh ray-trace, so every exposure reconstructs on an - identical source-pixel grid. - - Unlike the interferometer datacube case, the mapper, mapping matrix, curvature matrix and - regularization matrix are NOT shared — per-exposure PSFs and pixel offsets make the first three - per-dataset, and regularization may adapt to per-exposure data. - - Returns `None` when the analysis has not opted in (`shared_preloads=False`) or when the model performs - no inversion, in which case no state is shared and every factor fits as normal. - - The caller is responsible for the invariance contract: only enable `shared_preloads` when the factors - genuinely share the lens model, so the source-plane mesh really is exposure-invariant. The lead - factor's own `DatasetModel` offset (if any) is applied when the mesh is traced, so the mesh is defined - in the lead exposure's frame. - """ - if not self.shared_preloads: - return None - - fit = self.fit_from(instance=instance) - - if not fit.perform_inversion: - return None - - tracer_to_inversion = fit.tracer_to_inversion - - return aa.PreloadsImaging( - source_plane_mesh_grid=tracer_to_inversion.traced_mesh_grid_pg_list, - image_plane_mesh_grid=tracer_to_inversion.image_plane_mesh_grid_pg_list, - ) - - def fit_from( - self, - instance: af.ModelInstance, - preloads=None, - ) -> FitImaging: - """ - Given a model instance create a `FitImaging` object. - - This function is used in the `log_likelihood_function` to fit the model to the imaging data and compute the - log likelihood. - - Parameters - ---------- - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - preloads - An optional `PreloadsImaging` carrying the exposure-invariant source-plane mesh geometry, - computed once and reused by the fit instead of being rebuilt. Supplied by the multi-exposure - shared-state path (see `shared_state_from`); `None` (the default) fits as normal. - - Returns - ------- - FitImaging - The fit of the plane to the imaging dataset, which includes the log likelihood. - """ - - if self._use_jax: - self._register_fit_imaging_pytrees() - - tracer = self.tracer_via_instance_from( - instance=instance, - ) - - dataset_model = self.dataset_model_via_instance_from(instance=instance) - - adapt_images = self.adapt_images_via_instance_from( - instance=instance, - dataset_model=dataset_model, - galaxies=tracer.galaxies, - xp=self._xp, - ) - - return FitImaging( - dataset=self.dataset, - tracer=tracer, - dataset_model=dataset_model, - adapt_images=adapt_images, - settings=self.settings, - xp=self._xp, - preloads=preloads, - ) - - def save_attributes(self, paths: af.DirectoryPaths): - """ - Before the non-linear search begins, output the imaging ``dataset.fits`` - to the ``files`` folder so the aggregator loaders (e.g. ``ImagingAgg``, - ``agg_util.mask_header_from``) can always reload the dataset via - ``fit.value(name="dataset")``, independently of whether the visualization - ``fits_dataset`` output ran. The plotter interface also writes this file - to the ``image`` folder for inspection, but that write is gated on - visualization settings and is not guaranteed for every fit. - """ - super().save_attributes(paths=paths) - - image_list = [ - self.dataset.data.native_for_fits, - self.dataset.noise_map.native_for_fits, - self.dataset.psf.kernel.native_for_fits, - self.dataset.grids.lp.over_sample_size.native_for_fits.astype("float"), - self.dataset.grids.pixelization.over_sample_size.native_for_fits.astype( - "float" - ), - ] - - paths.save_fits( - name="dataset", - fits=hdu_list_for_output_from( - values_list=[image_list[0].mask.astype("float")] + image_list, - ext_name_list=[ - "mask", - "data", - "noise_map", - "psf", - "over_sample_size_lp", - "over_sample_size_pixelization", - ], - header_dict=self.dataset.mask.header_dict, - ), - ) - - @staticmethod - def _register_fit_imaging_pytrees() -> None: - """Register every type reachable from a ``FitImaging`` return value - so ``jax.jit(fit_from)`` can flatten its output. - - ``dataset``, ``adapt_images`` and ``settings`` are constants per - analysis — ride as aux so JAX does not recurse into them. Everything - else (``tracer``, ``dataset_model`` and the autoarray wrappers they - carry) is dynamic per fit. - - Idempotent — guarded by the module-level - ``_FIT_IMAGING_PYTREES_REGISTERED`` flag. ``DatasetModel`` and - ``Tracer`` may already be registered by - ``autofit.jax.pytrees.register_model`` (its - ``_REGISTERED_INSTANCE_CLASSES`` set is independent of autoarray's - ``_pytree_registered_classes``); cross-populate so - ``register_instance_pytree`` short-circuits. Mirrors the defense in - ``autogalaxy/ellipse/model/analysis.py``. - """ - global _FIT_IMAGING_PYTREES_REGISTERED - if _FIT_IMAGING_PYTREES_REGISTERED: - return - - from autoarray.abstract_ndarray import ( - register_instance_pytree, - _pytree_registered_classes, - ) - from autoarray.dataset.dataset_model import DatasetModel - from autolens.lens.tracer import Tracer - - try: - from autofit.jax.pytrees import ( - _REGISTERED_INSTANCE_CLASSES as _af_registered, - ) - except ImportError: - _af_registered = set() - - for cls in (DatasetModel, Tracer): - if cls in _af_registered: - _pytree_registered_classes.add(cls) - - register_instance_pytree( - FitImaging, - no_flatten=("dataset", "adapt_images", "settings", "preloads"), - ) - register_instance_pytree(DatasetModel) - # ``cosmology`` is a fixed physical constant per fit; ride as aux. - register_instance_pytree(Tracer, no_flatten=("cosmology",)) - - _FIT_IMAGING_PYTREES_REGISTERED = True - +""" +Analysis class for fitting a ``Tracer`` lens model to an imaging dataset. + +``AnalysisImaging`` implements the ``log_likelihood_function`` that a ``PyAutoFit`` +non-linear search calls on each iteration. It: + +1. Constructs a ``Tracer`` from the current model instance. +2. Optionally applies adaptive galaxy images to linear components. +3. Calls ``FitImaging`` to evaluate the log likelihood. +4. Returns the figure of merit (log likelihood or log evidence). + +It also manages result output (``ResultImaging``), on-the-fly visualisation +(``VisualizerImaging``), and position-based priors via ``PositionLikelihood``. +""" +import logging + +import autoarray as aa +import autofit as af +import autogalaxy as ag + +from autonerves.fitsable import hdu_list_for_output_from + +from autolens.analysis.analysis.dataset import AnalysisDataset +from autolens.analysis.exceptions import raise_fit_exception +from autolens.analysis.latent import LatentLens +from autolens.imaging.model.result import ResultImaging +from autolens.imaging.model.visualizer import VisualizerImaging +from autolens.imaging.fit_imaging import FitImaging + +logger = logging.getLogger(__name__) + +logger.setLevel(level="INFO") + + +_FIT_IMAGING_PYTREES_REGISTERED = False + + +class AnalysisImaging(AnalysisDataset): + + Result = ResultImaging + Visualizer = VisualizerImaging + Latent = LatentLens + + def __init__( + self, + dataset, + positions_likelihood_list=None, + adapt_images: ag.AdaptImages = None, + cosmology: ag.cosmo.LensingCosmology = None, + settings=None, + raise_inversion_positions_likelihood_exception: bool = True, + title_prefix: str = None, + use_jax: bool = True, + shared_preloads: bool = False, + **kwargs, + ): + """ + Fits a lens model to an imaging dataset via a non-linear search (see `AnalysisDataset` for + the full docstring of the shared parameters). + + Parameters + ---------- + shared_preloads + Opts this analysis into the cross-factor shared-state mechanism of a `FactorGraphModel` + (see `shared_state_from`). Set this to `True` only when this analysis is one of many + exposures of the same lens (e.g. multi-exposure imaging with per-exposure pixel offsets) + sharing an identical lens model, so the exposure-invariant source-plane mesh geometry + can be computed once and reused by every exposure. `False` by default, leaving the + standard per-analysis behaviour unchanged. + """ + super().__init__( + dataset=dataset, + positions_likelihood_list=positions_likelihood_list, + adapt_images=adapt_images, + cosmology=cosmology, + settings=settings, + raise_inversion_positions_likelihood_exception=raise_inversion_positions_likelihood_exception, + title_prefix=title_prefix, + use_jax=use_jax, + **kwargs, + ) + + self.shared_preloads = shared_preloads + + def log_likelihood_function(self, instance: af.ModelInstance, shared=None) -> float: + """ + Given an instance of the model, where the model parameters are set via a non-linear search, fit the model + instance to the imaging dataset. + + This function returns a log likelihood which is used by the non-linear search to guide the model-fit. + + For this analysis class, this function performs the following steps: + + 1) If the analysis has a adapt image, associated the model galaxy images of this dataset to the galaxies in + the model instance. + + 2) Extract attributes which model aspects of the data reductions, like the scaling the background sky + and background noise. + + 3) Extracts all galaxies from the model instance and set up a `Tracer`, which includes ordering the galaxies + by redshift to set up each `Plane`. + + 4) Use the `Tracer` and other attributes to create a `FitImaging` object, which performs steps such as creating + model images of every galaxy in the tracer, blurring them with the imaging dataset's PSF and computing + residuals, a chi-squared statistic and the log likelihood. + + Certain models will fail to fit the dataset and raise an exception. For example if an `Inversion` is used, the + linear algebra calculation may be invalid and raise an Exception. In such circumstances the model is discarded + and its likelihood value is passed to the non-linear search in a way that it ignores it (for example, using a + value of -1.0e99). + + Parameters + ---------- + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + shared + The cross-factor shared state of a `FactorGraphModel`, computed once per evaluation by the lead + factor's `shared_state_from` (see that method). For this analysis it is a `PreloadsImaging` + carrying the exposure-invariant source-plane mesh geometry; when provided it is reused by the fit + instead of being recomputed. `None` (the default, e.g. a standalone fit) leaves behaviour unchanged. + + Returns + ------- + float + The log likelihood indicating how well this model instance fitted the imaging data. + """ + + log_likelihood_penalty = self.log_likelihood_penalty_from( + instance=instance, + ) + + if self._use_jax: + return ( + self.fit_from(instance=instance, preloads=shared).figure_of_merit + - log_likelihood_penalty + ) + + try: + return ( + self.fit_from(instance=instance, preloads=shared).figure_of_merit + - log_likelihood_penalty + ) + except Exception as e: + raise_fit_exception(e) + + def shared_state_from(self, instance: af.ModelInstance): + """ + Compute the exposure-invariant source-plane mesh geometry once so it can be shared across the factors + of a multi-exposure `FactorGraphModel` (see `autofit.Analysis.shared_state_from`). + + When `shared_preloads` is set, every factor of the graph is an exposure of the same lens sharing an + identical lens model, so the source-plane mesh (the image-mesh centres of this lead exposure, + ray-traced through the shared lens model) is built once here and returned inside a `PreloadsImaging`, + which `FactorGraphModel` forwards as the `shared` argument to every factor's + `log_likelihood_function`. Each exposure then maps its own (offset) data grid onto the shared mesh + instead of computing its own image-mesh and mesh ray-trace, so every exposure reconstructs on an + identical source-pixel grid. + + Unlike the interferometer datacube case, the mapper, mapping matrix, curvature matrix and + regularization matrix are NOT shared — per-exposure PSFs and pixel offsets make the first three + per-dataset, and regularization may adapt to per-exposure data. + + Returns `None` when the analysis has not opted in (`shared_preloads=False`) or when the model performs + no inversion, in which case no state is shared and every factor fits as normal. + + The caller is responsible for the invariance contract: only enable `shared_preloads` when the factors + genuinely share the lens model, so the source-plane mesh really is exposure-invariant. The lead + factor's own `DatasetModel` offset (if any) is applied when the mesh is traced, so the mesh is defined + in the lead exposure's frame. + """ + if not self.shared_preloads: + return None + + fit = self.fit_from(instance=instance) + + if not fit.perform_inversion: + return None + + tracer_to_inversion = fit.tracer_to_inversion + + return aa.PreloadsImaging( + source_plane_mesh_grid=tracer_to_inversion.traced_mesh_grid_pg_list, + image_plane_mesh_grid=tracer_to_inversion.image_plane_mesh_grid_pg_list, + ) + + def fit_from( + self, + instance: af.ModelInstance, + preloads=None, + ) -> FitImaging: + """ + Given a model instance create a `FitImaging` object. + + This function is used in the `log_likelihood_function` to fit the model to the imaging data and compute the + log likelihood. + + Parameters + ---------- + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + preloads + An optional `PreloadsImaging` carrying the exposure-invariant source-plane mesh geometry, + computed once and reused by the fit instead of being rebuilt. Supplied by the multi-exposure + shared-state path (see `shared_state_from`); `None` (the default) fits as normal. + + Returns + ------- + FitImaging + The fit of the plane to the imaging dataset, which includes the log likelihood. + """ + + if self._use_jax: + self._register_fit_imaging_pytrees() + + tracer = self.tracer_via_instance_from( + instance=instance, + ) + + dataset_model = self.dataset_model_via_instance_from(instance=instance) + + adapt_images = self.adapt_images_via_instance_from( + instance=instance, + dataset_model=dataset_model, + galaxies=tracer.galaxies, + xp=self._xp, + ) + + return FitImaging( + dataset=self.dataset, + tracer=tracer, + dataset_model=dataset_model, + adapt_images=adapt_images, + settings=self.settings, + xp=self._xp, + preloads=preloads, + ) + + def save_attributes(self, paths: af.DirectoryPaths): + """ + Before the non-linear search begins, output the imaging ``dataset.fits`` + to the ``files`` folder so the aggregator loaders (e.g. ``ImagingAgg``, + ``agg_util.mask_header_from``) can always reload the dataset via + ``fit.value(name="dataset")``, independently of whether the visualization + ``fits_dataset`` output ran. The plotter interface also writes this file + to the ``image`` folder for inspection, but that write is gated on + visualization settings and is not guaranteed for every fit. + """ + super().save_attributes(paths=paths) + + image_list = [ + self.dataset.data.native_for_fits, + self.dataset.noise_map.native_for_fits, + self.dataset.psf.kernel.native_for_fits, + self.dataset.grids.lp.over_sample_size.native_for_fits.astype("float"), + self.dataset.grids.pixelization.over_sample_size.native_for_fits.astype( + "float" + ), + ] + + paths.save_fits( + name="dataset", + fits=hdu_list_for_output_from( + values_list=[image_list[0].mask.astype("float")] + image_list, + ext_name_list=[ + "mask", + "data", + "noise_map", + "psf", + "over_sample_size_lp", + "over_sample_size_pixelization", + ], + header_dict=self.dataset.mask.header_dict, + ), + ) + + @staticmethod + def _register_fit_imaging_pytrees() -> None: + """Register every type reachable from a ``FitImaging`` return value + so ``jax.jit(fit_from)`` can flatten its output. + + ``dataset``, ``adapt_images`` and ``settings`` are constants per + analysis — ride as aux so JAX does not recurse into them. Everything + else (``tracer``, ``dataset_model`` and the autoarray wrappers they + carry) is dynamic per fit. + + Idempotent — guarded by the module-level + ``_FIT_IMAGING_PYTREES_REGISTERED`` flag. ``DatasetModel`` and + ``Tracer`` may already be registered by + ``autofit.jax.pytrees.register_model`` (its + ``_REGISTERED_INSTANCE_CLASSES`` set is independent of autoarray's + ``_pytree_registered_classes``); cross-populate so + ``register_instance_pytree`` short-circuits. Mirrors the defense in + ``autogalaxy/ellipse/model/analysis.py``. + """ + global _FIT_IMAGING_PYTREES_REGISTERED + if _FIT_IMAGING_PYTREES_REGISTERED: + return + + from autoarray.abstract_ndarray import ( + register_instance_pytree, + _pytree_registered_classes, + ) + from autoarray.dataset.dataset_model import DatasetModel + from autolens.lens.tracer import Tracer + + try: + from autofit.jax.pytrees import ( + _REGISTERED_INSTANCE_CLASSES as _af_registered, + ) + except ImportError: + _af_registered = set() + + for cls in (DatasetModel, Tracer): + if cls in _af_registered: + _pytree_registered_classes.add(cls) + + register_instance_pytree( + FitImaging, + no_flatten=("dataset", "adapt_images", "settings", "preloads"), + ) + register_instance_pytree(DatasetModel) + # ``cosmology`` is a fixed physical constant per fit; ride as aux. + register_instance_pytree(Tracer, no_flatten=("cosmology",)) + + _FIT_IMAGING_PYTREES_REGISTERED = True + diff --git a/autolens/imaging/model/result.py b/autolens/imaging/model/result.py index 70c4f4f9a..7335dce9c 100644 --- a/autolens/imaging/model/result.py +++ b/autolens/imaging/model/result.py @@ -1,45 +1,45 @@ -import autoarray as aa - -from autolens.lens.tracer import Tracer -from autolens.imaging.fit_imaging import FitImaging -from autolens.analysis.result import ResultDataset - - -class ResultImaging(ResultDataset): - - @property - def max_log_likelihood_fit(self) -> FitImaging: - """ - An instance of a `FitImaging` corresponding to the maximum log likelihood model inferred by the non-linear - search. - """ - return self.analysis.fit_from( - instance=self.instance, - ) - - @property - def max_log_likelihood_tracer(self) -> Tracer: - """ - An instance of a `Tracer` corresponding to the maximum log likelihood model inferred by the non-linear search. - - The `Tracer` is computed from the `max_log_likelihood_fit`, as this ensures that all linear light profiles - are converted to normal light profiles with their `intensity` values updated. - """ - return ( - self.max_log_likelihood_fit.model_obj_linear_light_profiles_to_light_profiles - ) - - @property - def unmasked_model_image(self) -> aa.Array2D: - """ - The model image of the maximum log likelihood model, created without using a mask. - """ - return self.max_log_likelihood_fit.unmasked_blurred_image - - @property - def unmasked_model_image_of_planes(self): - """ - A list of the model image of every plane in the maximum log likelihood model, where all images are created - without using a mask. - """ - return self.max_log_likelihood_fit.unmasked_blurred_image_of_planes_list +import autoarray as aa + +from autolens.lens.tracer import Tracer +from autolens.imaging.fit_imaging import FitImaging +from autolens.analysis.result import ResultDataset + + +class ResultImaging(ResultDataset): + + @property + def max_log_likelihood_fit(self) -> FitImaging: + """ + An instance of a `FitImaging` corresponding to the maximum log likelihood model inferred by the non-linear + search. + """ + return self.analysis.fit_from( + instance=self.instance, + ) + + @property + def max_log_likelihood_tracer(self) -> Tracer: + """ + An instance of a `Tracer` corresponding to the maximum log likelihood model inferred by the non-linear search. + + The `Tracer` is computed from the `max_log_likelihood_fit`, as this ensures that all linear light profiles + are converted to normal light profiles with their `intensity` values updated. + """ + return ( + self.max_log_likelihood_fit.model_obj_linear_light_profiles_to_light_profiles + ) + + @property + def unmasked_model_image(self) -> aa.Array2D: + """ + The model image of the maximum log likelihood model, created without using a mask. + """ + return self.max_log_likelihood_fit.unmasked_blurred_image + + @property + def unmasked_model_image_of_planes(self): + """ + A list of the model image of every plane in the maximum log likelihood model, where all images are created + without using a mask. + """ + return self.max_log_likelihood_fit.unmasked_blurred_image_of_planes_list diff --git a/autolens/imaging/model/visualizer.py b/autolens/imaging/model/visualizer.py index e98f44505..46d4ce9b8 100644 --- a/autolens/imaging/model/visualizer.py +++ b/autolens/imaging/model/visualizer.py @@ -1,261 +1,261 @@ -import logging - -from autoarray import exc - -import autofit as af -import autogalaxy as ag - -from autolens.imaging.model.plotter import PlotterImaging -from autolens.imaging.plot.fit_imaging_plots import _compute_critical_curves_from_fit - -from autolens import exc - -logger = logging.getLogger(__name__) - -class VisualizerImaging(af.Visualizer): - @staticmethod - def visualize_before_fit( - analysis, - paths: af.AbstractPaths, - model: af.AbstractPriorModel, - ): - """ - PyAutoFit calls this function immediately before the non-linear search begins. - - It visualizes objects which do not change throughout the model fit like the dataset. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization and the pickled objects used by the aggregator output by this function. - model - The model object, which includes model components representing the galaxies that are fitted to - the imaging data. - """ - - plotter = PlotterImaging( - image_path=paths.image_path, title_prefix=analysis.title_prefix - ) - - plotter.imaging(dataset=analysis.dataset) - - if analysis.positions_likelihood_list is not None: - - positions_list = [] - - for positions_likelihood in analysis.positions_likelihood_list: - - positions_list.append( - positions_likelihood.positions - ) - - positions = ag.Grid2DIrregular(positions_list) - - plotter.image_with_positions( - image=analysis.dataset.data, - positions=positions, - ) - - if analysis.adapt_images is not None: - plotter.adapt_images(adapt_images=analysis.adapt_images) - - @staticmethod - def visualize( - analysis, - paths: af.DirectoryPaths, - instance: af.ModelInstance, - during_analysis: bool, - quick_update: bool = False, - ): - """ - Output images of the maximum log likelihood model inferred by the model-fit. This function is called throughout - the non-linear search at regular intervals, and therefore provides on-the-fly visualization of how well the - model-fit is going. - - The visualization performed by this function includes: - - - Images of the best-fit `Tracer`, including the images of each of its galaxies. - - - Images of the best-fit `FitImaging`, including the model-image, residuals and chi-squared of its fit to - the imaging data. - - - The adapt-images of the model-fit showing how the galaxies are used to represent different galaxies in - the dataset. - - The images output by this function are customized using the file `config/visualize/plots.yaml`. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization, and the pickled objects used by the aggregator output by this function. - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - """ - fit = analysis.fit_for_visualization(instance=instance) - tracer = fit.tracer_linear_light_profiles_to_light_profiles - - plotter = PlotterImaging( - image_path=paths.image_path, - title_prefix=analysis.title_prefix, - ) - - grid = fit.mask.derive_grid.all_false - - # Compute critical curves once for all plot functions. - ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curves_from_fit(fit) - - try: - plotter.fit_imaging( - fit=fit, - quick_update=quick_update, - image_plane_lines=ip_lines, - image_plane_line_colors=ip_colors, - source_plane_lines=sp_lines, - source_plane_line_colors=sp_colors, - ) - except exc.InversionException: - pass - - if quick_update: - return - - # Full update based on configs. - - if analysis.positions_likelihood_list is not None: - - overwrite_file = True - - for positions_likelihood in analysis.positions_likelihood_list: - - positions_likelihood.output_positions_info( - output_path=paths.output_path, tracer=fit.tracer, overwrite_file=overwrite_file - ) - - overwrite_file = False - - if fit.inversion is not None: - try: - fit.inversion.reconstruction - except exc.InversionException: - logger( - ag.exc.invalid_linear_algebra_for_visualization_message() - ) - return - - plotter.tracer( - tracer=tracer, - grid=grid, - image_plane_lines=ip_lines, - image_plane_line_colors=ip_colors, - source_plane_lines=sp_lines, - source_plane_line_colors=sp_colors, - ) - plotter.galaxies( - galaxies=tracer.galaxies, - grid=fit.grids.lp, - ) - if fit.inversion is not None: - if fit.inversion.has(cls=ag.Mapper): - plotter.inversion( - inversion=fit.inversion, - ) - - @staticmethod - def visualize_before_fit_combined( - analyses, - paths: af.AbstractPaths, - model: af.AbstractPriorModel, - ): - """ - Performs visualization before the non-linear search begins of information which shared across all analyses - on a single matplotlib figure. - - This function outputs visuals of all information which does not vary during the fit, for example the dataset - being fitted. - - Parameters - ---------- - analyses - The list of all analysis objects used for fitting via yhe non-linear search. - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization and the pickled objects used by the aggregator output by this function. - model - The model object, which includes model components representing the galaxies that are fitted to - the imaging data. - """ - - if analyses is None: - return - - plotter = PlotterImaging( - image_path=paths.image_path, title_prefix=analyses[0].title_prefix - ) - - dataset_list = [analysis.dataset for analysis in analyses] - - plotter.imaging_combined( - dataset_list=dataset_list, - ) - - @staticmethod - def visualize_combined( - analyses, - paths: af.AbstractPaths, - instance: af.ModelInstance, - during_analysis: bool, - quick_update: bool = False, - ): - """ - Performs visualization during the non-linear search of information which is shared across all analyses on a - single matplotlib figure. - - This function outputs visuals of all information which varies during the fit, for example the model-fit to - the dataset being fitted. - - Parameters - ---------- - analyses - The list of all analysis objects used for fitting via yhe non-linear search. - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization and the pickled objects used by the aggregator output by this function. - model - The model object, which includes model components representing the galaxies that are fitted to - the imaging data. - """ - - if analyses is None: - return - - # A mixed-dataset factor graph (e.g. imaging + weak lensing) routes every - # factor's analysis here; this combined subplot can only draw its own - # dataset type, so other analyses are skipped (each still visualizes its - # own fit individually). - from autolens.imaging.model.analysis import AnalysisImaging - - pairs = [ - (analysis, single_instance) - for analysis, single_instance in zip(analyses, instance) - if isinstance(analysis, AnalysisImaging) - ] - - if len(pairs) == 0: - return - - plotter = PlotterImaging( - image_path=paths.image_path, title_prefix=pairs[0][0].title_prefix - ) - - fit_list = [ - analysis.fit_for_visualization(instance=single_instance) - for analysis, single_instance in pairs - ] - - plotter.fit_imaging_combined( - fit_list=fit_list, - quick_update=quick_update - ) +import logging + +from autoarray import exc + +import autofit as af +import autogalaxy as ag + +from autolens.imaging.model.plotter import PlotterImaging +from autolens.imaging.plot.fit_imaging_plots import _compute_critical_curves_from_fit + +from autolens import exc + +logger = logging.getLogger(__name__) + +class VisualizerImaging(af.Visualizer): + @staticmethod + def visualize_before_fit( + analysis, + paths: af.AbstractPaths, + model: af.AbstractPriorModel, + ): + """ + PyAutoFit calls this function immediately before the non-linear search begins. + + It visualizes objects which do not change throughout the model fit like the dataset. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + model + The model object, which includes model components representing the galaxies that are fitted to + the imaging data. + """ + + plotter = PlotterImaging( + image_path=paths.image_path, title_prefix=analysis.title_prefix + ) + + plotter.imaging(dataset=analysis.dataset) + + if analysis.positions_likelihood_list is not None: + + positions_list = [] + + for positions_likelihood in analysis.positions_likelihood_list: + + positions_list.append( + positions_likelihood.positions + ) + + positions = ag.Grid2DIrregular(positions_list) + + plotter.image_with_positions( + image=analysis.dataset.data, + positions=positions, + ) + + if analysis.adapt_images is not None: + plotter.adapt_images(adapt_images=analysis.adapt_images) + + @staticmethod + def visualize( + analysis, + paths: af.DirectoryPaths, + instance: af.ModelInstance, + during_analysis: bool, + quick_update: bool = False, + ): + """ + Output images of the maximum log likelihood model inferred by the model-fit. This function is called throughout + the non-linear search at regular intervals, and therefore provides on-the-fly visualization of how well the + model-fit is going. + + The visualization performed by this function includes: + + - Images of the best-fit `Tracer`, including the images of each of its galaxies. + + - Images of the best-fit `FitImaging`, including the model-image, residuals and chi-squared of its fit to + the imaging data. + + - The adapt-images of the model-fit showing how the galaxies are used to represent different galaxies in + the dataset. + + The images output by this function are customized using the file `config/visualize/plots.yaml`. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization, and the pickled objects used by the aggregator output by this function. + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + """ + fit = analysis.fit_for_visualization(instance=instance) + tracer = fit.tracer_linear_light_profiles_to_light_profiles + + plotter = PlotterImaging( + image_path=paths.image_path, + title_prefix=analysis.title_prefix, + ) + + grid = fit.mask.derive_grid.all_false + + # Compute critical curves once for all plot functions. + ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curves_from_fit(fit) + + try: + plotter.fit_imaging( + fit=fit, + quick_update=quick_update, + image_plane_lines=ip_lines, + image_plane_line_colors=ip_colors, + source_plane_lines=sp_lines, + source_plane_line_colors=sp_colors, + ) + except exc.InversionException: + pass + + if quick_update: + return + + # Full update based on configs. + + if analysis.positions_likelihood_list is not None: + + overwrite_file = True + + for positions_likelihood in analysis.positions_likelihood_list: + + positions_likelihood.output_positions_info( + output_path=paths.output_path, tracer=fit.tracer, overwrite_file=overwrite_file + ) + + overwrite_file = False + + if fit.inversion is not None: + try: + fit.inversion.reconstruction + except exc.InversionException: + logger( + ag.exc.invalid_linear_algebra_for_visualization_message() + ) + return + + plotter.tracer( + tracer=tracer, + grid=grid, + image_plane_lines=ip_lines, + image_plane_line_colors=ip_colors, + source_plane_lines=sp_lines, + source_plane_line_colors=sp_colors, + ) + plotter.galaxies( + galaxies=tracer.galaxies, + grid=fit.grids.lp, + ) + if fit.inversion is not None: + if fit.inversion.has(cls=ag.Mapper): + plotter.inversion( + inversion=fit.inversion, + ) + + @staticmethod + def visualize_before_fit_combined( + analyses, + paths: af.AbstractPaths, + model: af.AbstractPriorModel, + ): + """ + Performs visualization before the non-linear search begins of information which shared across all analyses + on a single matplotlib figure. + + This function outputs visuals of all information which does not vary during the fit, for example the dataset + being fitted. + + Parameters + ---------- + analyses + The list of all analysis objects used for fitting via yhe non-linear search. + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + model + The model object, which includes model components representing the galaxies that are fitted to + the imaging data. + """ + + if analyses is None: + return + + plotter = PlotterImaging( + image_path=paths.image_path, title_prefix=analyses[0].title_prefix + ) + + dataset_list = [analysis.dataset for analysis in analyses] + + plotter.imaging_combined( + dataset_list=dataset_list, + ) + + @staticmethod + def visualize_combined( + analyses, + paths: af.AbstractPaths, + instance: af.ModelInstance, + during_analysis: bool, + quick_update: bool = False, + ): + """ + Performs visualization during the non-linear search of information which is shared across all analyses on a + single matplotlib figure. + + This function outputs visuals of all information which varies during the fit, for example the model-fit to + the dataset being fitted. + + Parameters + ---------- + analyses + The list of all analysis objects used for fitting via yhe non-linear search. + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + model + The model object, which includes model components representing the galaxies that are fitted to + the imaging data. + """ + + if analyses is None: + return + + # A mixed-dataset factor graph (e.g. imaging + weak lensing) routes every + # factor's analysis here; this combined subplot can only draw its own + # dataset type, so other analyses are skipped (each still visualizes its + # own fit individually). + from autolens.imaging.model.analysis import AnalysisImaging + + pairs = [ + (analysis, single_instance) + for analysis, single_instance in zip(analyses, instance) + if isinstance(analysis, AnalysisImaging) + ] + + if len(pairs) == 0: + return + + plotter = PlotterImaging( + image_path=paths.image_path, title_prefix=pairs[0][0].title_prefix + ) + + fit_list = [ + analysis.fit_for_visualization(instance=single_instance) + for analysis, single_instance in pairs + ] + + plotter.fit_imaging_combined( + fit_list=fit_list, + quick_update=quick_update + ) diff --git a/autolens/imaging/simulator.py b/autolens/imaging/simulator.py index 3ed891f6d..f647f660c 100644 --- a/autolens/imaging/simulator.py +++ b/autolens/imaging/simulator.py @@ -1,213 +1,213 @@ -""" -Imaging simulator for strong gravitational lens observations. - -``SimulatorImaging`` extends ``aa.SimulatorImaging`` with a ``via_tracer_from`` method -that accepts a ``Tracer`` object and: - -1. Uses the tracer's mass profiles to ray-trace the image-plane grid to the source plane. -2. Evaluates the light profiles of all galaxies on the (traced) grid. -3. Passes the result through the standard ``SimulatorImaging`` pipeline: PSF convolution, - background sky addition, and Poisson noise realisation. - -This is the primary entry point for generating synthetic lens datasets for testing, -validation, and mock-data studies. -""" -import numpy as np -from typing import List - -import autoarray as aa -import autogalaxy as ag - -from autolens.lens.tracer import Tracer - -class SimulatorImaging(aa.SimulatorImaging): - - def via_tracer_from(self, tracer : Tracer, grid : aa.type.Grid2DLike, xp=None) -> aa.Imaging: - """ - Simulate an `Imaging` dataset from an input `Tracer` object and a 2D grid of (y,x) coordinates. - - The mass profiles of each galaxy in the tracer are used to perform ray-tracing of the input 2D grid and - their light profiles are used to generate the image of the galaxies which are simulated. - - The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are - described in the `SimulatorImaging` `__init__` method docstring, found in the PyAutoArray project. - - If one of more galaxy light profiles are a `LightProfileSNR` object, the `intensity` of the light profile is - automatically set such that the signal-to-noise ratio of the light profile is equal to its input - `signal_to_noise_ratio` value. - - For example, if a `LightProfileSNR` object has a `signal_to_noise_ratio` of 5.0, the intensity of the light - profile is set such that the peak surface brightness of the profile is 5.0 times the background noise level of - the image. - - Parameters - ---------- - tracer - The tracer, which describes the ray-tracing and strong lens configuration used to simulate the imaging - dataset as well as the light profiles of the galaxies used to simulate the image of the galaxies. - grid - The 2D grid of (y,x) coordinates which the mass profiles of the galaxies in the tracer are ray-traced using - in order to generate the image of the galaxies via their light profiles. - """ - - if xp is None: - xp = self._xp - - tracer.set_snr_of_snr_light_profiles( - grid=grid, - exposure_time=self.exposure_time, - background_sky_level=self.background_sky_level, - ) - - if self.psf.convolve_over_sample_size > 1: - - image = tracer.convolved_padded_image_2d_from( - grid=grid, psf=self.psf, xp=xp - ) - - over_sample_size = grid.over_sample_size.resized_from( - new_shape=image.shape_native, mask_pad_value=1 - ) - - dataset = self.via_image_from( - image=image, - over_sample_size=over_sample_size, - image_is_convolved=True, - xp=xp, - ) - - return dataset.trimmed_after_convolution_from( - kernel_shape=self.psf.kernel_shape_image_resolution - ) - - image = tracer.padded_image_2d_from( - grid=grid, psf_shape_2d=self.psf.kernel.shape_native, xp=xp - ) - - over_sample_size = grid.over_sample_size.resized_from( - new_shape=image.shape_native, mask_pad_value=1 - ) - - dataset = self.via_image_from(image=image, over_sample_size=over_sample_size, xp=xp) - - return dataset.trimmed_after_convolution_from( - kernel_shape=self.psf.kernel.shape_native - ) - - def via_galaxies_from(self, galaxies : List[ag.Galaxy], grid : aa.type.Grid2DLike) -> aa.Imaging: - """ - Simulate an `Imaging` dataset from an input list of `Galaxy` objects and a 2D grid of (y,x) coordinates. - - The galaxies are used to create a `Tracer`. The mass profiles of each galaxy in the tracer are used to - perform ray-tracing of the input 2D grid and their light profiles are used to generate the image of the - galaxies which are simulated. - - The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are - described in the `SimulatorImaging` `__init__` method docstring. - - If one of more galaxy light profiles are a `LightProfileSNR` object, the `intensity` of the light profile is - automatically set such that the signal-to-noise ratio of the light profile is equal to its input - `signal_to_noise_ratio` value. - - For example, if a `LightProfileSNR` object has a `signal_to_noise_ratio` of 5.0, the intensity of the light - profile is set such that the peak surface brightness of the profile is 5.0 times the background noise level of - the image. - - Parameters - ---------- - galaxies - The galaxies used to create the tracer, which describes the ray-tracing and strong lens configuration - used to simulate the imaging dataset. - grid - The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. - """ - - tracer = Tracer(galaxies=galaxies) - - return self.via_tracer_from(tracer=tracer, grid=grid) - - def via_deflections_and_galaxies_from(self, deflections : aa.VectorYX2D, galaxies : List[ag.Galaxy]) -> aa.Imaging: - """ - Simulate an `Imaging` dataset from an input deflection angle map and list of galaxies. - - The input deflection angle map ray-traces the image-plane coordinates from the image-plane to source-plane, - via the lens equation. - - This traced grid is then used to evaluate the light of the list of galaxies, which therefore simulate the - image of the strong lens. - - This function is used in situations where one has access to a deflection angle map which does not suit being - ray-traced using a `Tracer` object (e.g. deflection angles from a cosmological simulation of a galaxy). - - The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are - described in the `SimulatorImaging` `__init__` method docstring. - - Parameters - ---------- - galaxies - The galaxies used to create the tracer, which describes the ray-tracing and strong lens configuration - used to simulate the imaging dataset. - grid - The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. - """ - grid = aa.Grid2D.uniform( - shape_native=deflections.shape_native, - pixel_scales=deflections.pixel_scales, - over_sample_size=1 - ) - - deflected_grid = aa.Grid2D( - values=grid - deflections, - mask=grid.mask, - over_sample_size=1, - over_sampled=grid - deflections, - over_sampler=grid.over_sampler - ) - - image = sum(map(lambda g: g.image_2d_from(grid=deflected_grid), galaxies)) - - return self.via_image_from(image=image) - - def via_source_image_from(self, tracer : Tracer, grid : aa.type.Grid2DLike, source_image : aa.Array2D) -> aa.Imaging: - """ - Simulate an `Imaging` dataset from an input image of a source galaxy. - - This input image is on a uniform and regular 2D array, meaning it can simulate the source's irregular - and asymmetric source galaxy morphological features. - - The typical use case is inputting the image of an irregular galaxy in the source-plane (whose values are - on a uniform array) and using this function to compute the lensed image of this source galaxy. - - The tracer is used to perform ray-tracing and generate the image of the strong lens galaxies (e.g. - the lens light, lensed source light, etc) which is simulated. - - The source galaxy light profiles are ignored in favour of the input source image, but the emission of - other galaxies (e.g. the lems galaxy's light) are included. - - The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are - described in the `SimulatorImaging` `__init__` method docstring. - - Parameters - ---------- - tracer - The tracer, which describes the ray-tracing and strong lens configuration used to simulate the imaging - dataset. - grid - The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. - source_image - The image of the source-plane and source galaxy which is interpolated to compute the lensed image. - """ - - image = tracer.image_2d_via_input_plane_image_from( - grid=grid, - plane_image=source_image - ) - - padded_image = image.padded_before_convolution_from( - kernel_shape=self.psf.kernel.shape_native - ) - dataset = self.via_image_from(image=padded_image) - - return dataset.trimmed_after_convolution_from( - kernel_shape=self.psf.kernel.shape_native +""" +Imaging simulator for strong gravitational lens observations. + +``SimulatorImaging`` extends ``aa.SimulatorImaging`` with a ``via_tracer_from`` method +that accepts a ``Tracer`` object and: + +1. Uses the tracer's mass profiles to ray-trace the image-plane grid to the source plane. +2. Evaluates the light profiles of all galaxies on the (traced) grid. +3. Passes the result through the standard ``SimulatorImaging`` pipeline: PSF convolution, + background sky addition, and Poisson noise realisation. + +This is the primary entry point for generating synthetic lens datasets for testing, +validation, and mock-data studies. +""" +import numpy as np +from typing import List + +import autoarray as aa +import autogalaxy as ag + +from autolens.lens.tracer import Tracer + +class SimulatorImaging(aa.SimulatorImaging): + + def via_tracer_from(self, tracer : Tracer, grid : aa.type.Grid2DLike, xp=None) -> aa.Imaging: + """ + Simulate an `Imaging` dataset from an input `Tracer` object and a 2D grid of (y,x) coordinates. + + The mass profiles of each galaxy in the tracer are used to perform ray-tracing of the input 2D grid and + their light profiles are used to generate the image of the galaxies which are simulated. + + The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are + described in the `SimulatorImaging` `__init__` method docstring, found in the PyAutoArray project. + + If one of more galaxy light profiles are a `LightProfileSNR` object, the `intensity` of the light profile is + automatically set such that the signal-to-noise ratio of the light profile is equal to its input + `signal_to_noise_ratio` value. + + For example, if a `LightProfileSNR` object has a `signal_to_noise_ratio` of 5.0, the intensity of the light + profile is set such that the peak surface brightness of the profile is 5.0 times the background noise level of + the image. + + Parameters + ---------- + tracer + The tracer, which describes the ray-tracing and strong lens configuration used to simulate the imaging + dataset as well as the light profiles of the galaxies used to simulate the image of the galaxies. + grid + The 2D grid of (y,x) coordinates which the mass profiles of the galaxies in the tracer are ray-traced using + in order to generate the image of the galaxies via their light profiles. + """ + + if xp is None: + xp = self._xp + + tracer.set_snr_of_snr_light_profiles( + grid=grid, + exposure_time=self.exposure_time, + background_sky_level=self.background_sky_level, + ) + + if self.psf.convolve_over_sample_size > 1: + + image = tracer.convolved_padded_image_2d_from( + grid=grid, psf=self.psf, xp=xp + ) + + over_sample_size = grid.over_sample_size.resized_from( + new_shape=image.shape_native, mask_pad_value=1 + ) + + dataset = self.via_image_from( + image=image, + over_sample_size=over_sample_size, + image_is_convolved=True, + xp=xp, + ) + + return dataset.trimmed_after_convolution_from( + kernel_shape=self.psf.kernel_shape_image_resolution + ) + + image = tracer.padded_image_2d_from( + grid=grid, psf_shape_2d=self.psf.kernel.shape_native, xp=xp + ) + + over_sample_size = grid.over_sample_size.resized_from( + new_shape=image.shape_native, mask_pad_value=1 + ) + + dataset = self.via_image_from(image=image, over_sample_size=over_sample_size, xp=xp) + + return dataset.trimmed_after_convolution_from( + kernel_shape=self.psf.kernel.shape_native + ) + + def via_galaxies_from(self, galaxies : List[ag.Galaxy], grid : aa.type.Grid2DLike) -> aa.Imaging: + """ + Simulate an `Imaging` dataset from an input list of `Galaxy` objects and a 2D grid of (y,x) coordinates. + + The galaxies are used to create a `Tracer`. The mass profiles of each galaxy in the tracer are used to + perform ray-tracing of the input 2D grid and their light profiles are used to generate the image of the + galaxies which are simulated. + + The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are + described in the `SimulatorImaging` `__init__` method docstring. + + If one of more galaxy light profiles are a `LightProfileSNR` object, the `intensity` of the light profile is + automatically set such that the signal-to-noise ratio of the light profile is equal to its input + `signal_to_noise_ratio` value. + + For example, if a `LightProfileSNR` object has a `signal_to_noise_ratio` of 5.0, the intensity of the light + profile is set such that the peak surface brightness of the profile is 5.0 times the background noise level of + the image. + + Parameters + ---------- + galaxies + The galaxies used to create the tracer, which describes the ray-tracing and strong lens configuration + used to simulate the imaging dataset. + grid + The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. + """ + + tracer = Tracer(galaxies=galaxies) + + return self.via_tracer_from(tracer=tracer, grid=grid) + + def via_deflections_and_galaxies_from(self, deflections : aa.VectorYX2D, galaxies : List[ag.Galaxy]) -> aa.Imaging: + """ + Simulate an `Imaging` dataset from an input deflection angle map and list of galaxies. + + The input deflection angle map ray-traces the image-plane coordinates from the image-plane to source-plane, + via the lens equation. + + This traced grid is then used to evaluate the light of the list of galaxies, which therefore simulate the + image of the strong lens. + + This function is used in situations where one has access to a deflection angle map which does not suit being + ray-traced using a `Tracer` object (e.g. deflection angles from a cosmological simulation of a galaxy). + + The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are + described in the `SimulatorImaging` `__init__` method docstring. + + Parameters + ---------- + galaxies + The galaxies used to create the tracer, which describes the ray-tracing and strong lens configuration + used to simulate the imaging dataset. + grid + The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. + """ + grid = aa.Grid2D.uniform( + shape_native=deflections.shape_native, + pixel_scales=deflections.pixel_scales, + over_sample_size=1 + ) + + deflected_grid = aa.Grid2D( + values=grid - deflections, + mask=grid.mask, + over_sample_size=1, + over_sampled=grid - deflections, + over_sampler=grid.over_sampler + ) + + image = sum(map(lambda g: g.image_2d_from(grid=deflected_grid), galaxies)) + + return self.via_image_from(image=image) + + def via_source_image_from(self, tracer : Tracer, grid : aa.type.Grid2DLike, source_image : aa.Array2D) -> aa.Imaging: + """ + Simulate an `Imaging` dataset from an input image of a source galaxy. + + This input image is on a uniform and regular 2D array, meaning it can simulate the source's irregular + and asymmetric source galaxy morphological features. + + The typical use case is inputting the image of an irregular galaxy in the source-plane (whose values are + on a uniform array) and using this function to compute the lensed image of this source galaxy. + + The tracer is used to perform ray-tracing and generate the image of the strong lens galaxies (e.g. + the lens light, lensed source light, etc) which is simulated. + + The source galaxy light profiles are ignored in favour of the input source image, but the emission of + other galaxies (e.g. the lems galaxy's light) are included. + + The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are + described in the `SimulatorImaging` `__init__` method docstring. + + Parameters + ---------- + tracer + The tracer, which describes the ray-tracing and strong lens configuration used to simulate the imaging + dataset. + grid + The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. + source_image + The image of the source-plane and source galaxy which is interpolated to compute the lensed image. + """ + + image = tracer.image_2d_via_input_plane_image_from( + grid=grid, + plane_image=source_image + ) + + padded_image = image.padded_before_convolution_from( + kernel_shape=self.psf.kernel.shape_native + ) + dataset = self.via_image_from(image=padded_image) + + return dataset.trimmed_after_convolution_from( + kernel_shape=self.psf.kernel.shape_native ) \ No newline at end of file diff --git a/autolens/interferometer/fit_interferometer.py b/autolens/interferometer/fit_interferometer.py index 3df2364c8..dbe659354 100644 --- a/autolens/interferometer/fit_interferometer.py +++ b/autolens/interferometer/fit_interferometer.py @@ -1,310 +1,310 @@ -""" -Interferometer fit class for strong gravitational lens modeling in the uv-plane. - -``FitInterferometer`` extends the ``autogalaxy`` ``FitInterferometer`` base class to -work with a ``Tracer`` instead of a plain ``Galaxies`` collection. The fit pipeline -mirrors the imaging analogue but operates entirely in the visibility (uv) domain: - -1. Evaluate all light profiles of the tracer galaxies on the (ray-traced) real-space grid. -2. Apply the Fourier transform (DFT or NUFFT) to map the image to visibilities. -3. Subtract the predicted visibilities from the observed visibilities. -4. If the tracer contains linear light profiles or pixelizations, solve for their - amplitudes via a linear inversion of the residual visibilities. -5. Combine direct and inversion visibilities into the ``model_data``. -6. Compute residuals, chi-squared, and log likelihood (or log evidence). - -The ``TracerToInversion`` helper is used to assemble the linear system in step 4. -""" -import numpy as np -from typing import Dict, List, Optional - -from autonerves import cached_property - -import autoarray as aa -import autogalaxy as ag - -from autogalaxy.abstract_fit import AbstractFitInversion - -from autolens.lens.tracer import Tracer -from autolens.lens.to_inversion import TracerToInversion - - -class FitInterferometer(aa.FitInterferometer, AbstractFitInversion): - def __init__( - self, - dataset: aa.Interferometer, - tracer: Tracer, - dataset_model: Optional[aa.DatasetModel] = None, - adapt_images: Optional[ag.AdaptImages] = None, - settings: aa.Settings = None, - xp=np, - preloads=None, - ): - """ - Fits an interferometer dataset using a `Tracer` object. - - The fit performs the following steps: - - 1) Compute the sum of all images of galaxy light profiles in the `Tracer`. - - 2) Fourier transform this image with the transformer object and `uv_wavelengths` to create - the `profile_visibilities`. - - 3) Subtract these visibilities from the `data` to create the `profile_subtracted_visibilities`. - - 4) If the `Tracer` has any linear algebra objects (e.g. linear light profiles, a pixelization / regulariation) - fit the `profile_subtracted_visibilities` with these objects via an inversion. - - 5) Compute the `model_data` as the sum of the `profile_visibilities` and `reconstructed_data` of the inversion - (if an inversion is not performed the `model_data` is only the `profile_visibilities`. - - 6) Subtract the `model_data` from the data and compute the residuals, chi-squared and likelihood via the - noise-map (if an inversion is performed the `log_evidence`, including addition terms describing the linear - algebra solution, is computed). - - When performing a model-fit` via ` AnalysisInterferometer` object the `figure_of_merit` of - this object is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - dataset - The interforometer dataset which is fitted by the galaxies in the tracer. - tracer - The tracer of galaxies whose light profile images are used to fit the interferometer data. - dataset_model - Attributes which allow for parts of a dataset to be treated as a model (e.g. the background sky level). - adapt_images - Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the - reconstructed galaxy's morphology. - settings - Settings controlling how an inversion is fitted for example which linear algebra formalism is used. - preloads - An optional `PreloadsInterferometer` carrying channel-invariant inversion quantities (e.g. the - `curvature_matrix` `F`) computed once and reused by this fit instead of being rebuilt. Used by the - datacube shared-state path, where every spectral channel shares the lens model. `None` (the - default) leaves the standard per-fit behaviour unchanged. - """ - - self.tracer = tracer - - self.adapt_images = adapt_images - - self.settings = settings - - self.preloads = preloads - - super().__init__( - dataset=dataset, - dataset_model=dataset_model, - xp=xp, - ) - AbstractFitInversion.__init__( - self=self, model_obj=tracer, settings=settings, xp=xp - ) - - self.use_jax = xp is not np - - @property - def _xp(self): - if self.use_jax: - import jax.numpy as jnp - - return jnp - return np - - @property - def profile_visibilities(self) -> aa.Visibilities: - """ - Returns the visibilities of every light profile in the tracer, which are computed by performing a Fourier - transform to the sum of light profile images. - """ - return self.tracer.visibilities_from( - grid=self.grids.lp, transformer=self.dataset.transformer, xp=self._xp - ) - - @property - def profile_subtracted_visibilities(self) -> aa.Visibilities: - """ - Returns the interferometer dataset's visibilities with all transformed light profile images in the fit's - tracer subtracted. - """ - return self.data - self.profile_visibilities - - @property - def tracer_to_inversion(self) -> TracerToInversion: - dataset = aa.DatasetInterface( - data=self.profile_subtracted_visibilities, - noise_map=self.noise_map, - grids=self.grids, - transformer=self.dataset.transformer, - sparse_operator=self.dataset.sparse_operator, - ) - - return TracerToInversion( - dataset=dataset, - tracer=self.tracer, - adapt_images=self.adapt_images, - settings=self.settings, - xp=self._xp, - preloads=self._preloads_scoped, - ) - - @property - def _preloads_scoped(self): - """ - The preloads as consumed by this fit's inversion, scoped to its dataset type. - - Cross-dataset-type shared state (e.g. an imaging lead factor in a joint - imaging + interferometer graph) is valid here only through its source-plane mesh - geometry — a mapper / curvature matrix from another dataset type would embed that - dataset's grids and silently corrupt the fit, so non-interferometer preloads are - reduced to their mesh-geometry view. - """ - if self.preloads is None or isinstance(self.preloads, aa.PreloadsInterferometer): - return self.preloads - - return aa.PreloadsInterferometer( - source_plane_mesh_grid=self.preloads.source_plane_mesh_grid, - image_plane_mesh_grid=self.preloads.image_plane_mesh_grid, - ) - - @cached_property - def inversion(self) -> Optional[aa.AbstractInversion]: - """ - If the tracer has linear objects which are used to fit the data (e.g. a linear light profile / pixelization) - this function returns a linear inversion, where the flux values of these objects (e.g. the `intensity` - of linear light profiles) are computed via linear matrix algebra. - - The data passed to this function is the dataset's image with all light profile images of the tracer subtracted, - ensuring that the inversion only fits the data with ordinary light profiles subtracted. - """ - if self.perform_inversion: - return self.tracer_to_inversion.inversion - - @property - def model_data(self) -> aa.Visibilities: - """ - Returns the model data that is used to fit the data. - - If the tracer does not have any linear objects and therefore omits an inversion, the model data is the - sum of all light profile images Fourier transformed to visibilities. - - If a inversion is included it is the sum of these visibilities and the inversion's reconstructed visibilities. - """ - - if self.perform_inversion: - return ( - self.profile_visibilities - + self.inversion.mapped_reconstructed_operated_data - ) - - return self.profile_visibilities - - @property - def galaxy_image_dict(self) -> Dict[ag.Galaxy, np.ndarray]: - """ - A dictionary which associates every galaxy in the tracer with its `image`. - - This image is the image of the sum of: - - - The images of all ordinary light profiles in that tracer summed. - - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved - for first via the inversion. - - For modeling, this dictionary is used to set up the `adapt_images` that adapt certain pixelizations to the - data being fitted. - """ - galaxy_image_dict = self.tracer.galaxy_image_2d_dict_from( - grid=self.grids.lp, xp=self._xp - ) - - galaxy_linear_obj_image_dict = self.galaxy_linear_obj_data_dict_from( - use_operated=False - ) - - return {**galaxy_image_dict, **galaxy_linear_obj_image_dict} - - @property - def galaxy_signal_to_noise_map_dict(self) -> Dict[ag.Galaxy, np.ndarray]: - """ - A dictionary which associates every galaxy in the tracer with its signal-to-noise map. - - This signal-to-noise map is the signal-to-noise map of the sum of: - - - The images of all ordinary light profiles in that tracer summed. - - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved - for first via the inversion. - - For modeling, this dictionary is used to set up the `adapt_images` that adapt certain pixelizations to the - data being fitted. - """ - galaxy_image_dict = self.galaxy_image_dict - - galaxy_signal_to_noise_map_dict = {} - - for galaxy, image in galaxy_image_dict.items(): - galaxy_signal_to_noise_map_dict[galaxy] = image / self.dirty_noise_map - - return galaxy_signal_to_noise_map_dict - - @property - def galaxy_model_visibilities_dict(self) -> Dict[ag.Galaxy, np.ndarray]: - """ - A dictionary which associates every galaxy in the tracer with its model visibilities. - - These visibilities are the sum of: - - - The visibilities of all ordinary light profiles in that tracer summed and Fourier transformed to visibilities - space. - - The visibilities of all linear objects (e.g. linear light profiles / pixelizations), where the visibilities - are solved for first via the inversion. - """ - galaxy_model_visibilities_dict = self.tracer.galaxy_visibilities_dict_from( - grid=self.grids.lp, transformer=self.dataset.transformer, xp=self._xp - ) - - galaxy_linear_obj_visibilities_dict = self.galaxy_linear_obj_data_dict_from( - use_operated=True - ) - - return {**galaxy_model_visibilities_dict, **galaxy_linear_obj_visibilities_dict} - - @property - def model_visibilities_of_planes_list(self) -> List[aa.Visibilities]: - """ - A list of every model image of every plane in the tracer. - - This image is the image of the sum of: - - - The images of all ordinary light profiles in that plane summed and convolved with the imaging data's PSF. - - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved - for first via the inversion. - - This is used to visualize the different contibutions of light from the image-plane, source-plane and other - planes in a fit. - """ - galaxy_model_visibilities_dict = self.galaxy_model_visibilities_dict - - model_visibilities_of_planes_list = [ - aa.Visibilities.zeros(shape_slim=(self.dataset.data.shape_slim,)) - for i in range(self.tracer.total_planes) - ] - - for plane_index, galaxies in enumerate(self.tracer.planes): - for galaxy in galaxies: - model_visibilities_of_planes_list[ - plane_index - ] += galaxy_model_visibilities_dict[galaxy] - - return model_visibilities_of_planes_list - - @property - def tracer_linear_light_profiles_to_light_profiles(self) -> Tracer: - """ - The `Tracer` where all linear light profiles have been converted to ordinary light profiles, where their - `intensity` values are set to the values inferred by this fit. - - This is typically used for visualization, because linear light profiles cannot be used in `LightProfile` - or `Galaxy` objects. - """ - return self.model_obj_linear_light_profiles_to_light_profiles +""" +Interferometer fit class for strong gravitational lens modeling in the uv-plane. + +``FitInterferometer`` extends the ``autogalaxy`` ``FitInterferometer`` base class to +work with a ``Tracer`` instead of a plain ``Galaxies`` collection. The fit pipeline +mirrors the imaging analogue but operates entirely in the visibility (uv) domain: + +1. Evaluate all light profiles of the tracer galaxies on the (ray-traced) real-space grid. +2. Apply the Fourier transform (DFT or NUFFT) to map the image to visibilities. +3. Subtract the predicted visibilities from the observed visibilities. +4. If the tracer contains linear light profiles or pixelizations, solve for their + amplitudes via a linear inversion of the residual visibilities. +5. Combine direct and inversion visibilities into the ``model_data``. +6. Compute residuals, chi-squared, and log likelihood (or log evidence). + +The ``TracerToInversion`` helper is used to assemble the linear system in step 4. +""" +import numpy as np +from typing import Dict, List, Optional + +from autonerves import cached_property + +import autoarray as aa +import autogalaxy as ag + +from autogalaxy.abstract_fit import AbstractFitInversion + +from autolens.lens.tracer import Tracer +from autolens.lens.to_inversion import TracerToInversion + + +class FitInterferometer(aa.FitInterferometer, AbstractFitInversion): + def __init__( + self, + dataset: aa.Interferometer, + tracer: Tracer, + dataset_model: Optional[aa.DatasetModel] = None, + adapt_images: Optional[ag.AdaptImages] = None, + settings: aa.Settings = None, + xp=np, + preloads=None, + ): + """ + Fits an interferometer dataset using a `Tracer` object. + + The fit performs the following steps: + + 1) Compute the sum of all images of galaxy light profiles in the `Tracer`. + + 2) Fourier transform this image with the transformer object and `uv_wavelengths` to create + the `profile_visibilities`. + + 3) Subtract these visibilities from the `data` to create the `profile_subtracted_visibilities`. + + 4) If the `Tracer` has any linear algebra objects (e.g. linear light profiles, a pixelization / regulariation) + fit the `profile_subtracted_visibilities` with these objects via an inversion. + + 5) Compute the `model_data` as the sum of the `profile_visibilities` and `reconstructed_data` of the inversion + (if an inversion is not performed the `model_data` is only the `profile_visibilities`. + + 6) Subtract the `model_data` from the data and compute the residuals, chi-squared and likelihood via the + noise-map (if an inversion is performed the `log_evidence`, including addition terms describing the linear + algebra solution, is computed). + + When performing a model-fit` via ` AnalysisInterferometer` object the `figure_of_merit` of + this object is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + dataset + The interforometer dataset which is fitted by the galaxies in the tracer. + tracer + The tracer of galaxies whose light profile images are used to fit the interferometer data. + dataset_model + Attributes which allow for parts of a dataset to be treated as a model (e.g. the background sky level). + adapt_images + Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the + reconstructed galaxy's morphology. + settings + Settings controlling how an inversion is fitted for example which linear algebra formalism is used. + preloads + An optional `PreloadsInterferometer` carrying channel-invariant inversion quantities (e.g. the + `curvature_matrix` `F`) computed once and reused by this fit instead of being rebuilt. Used by the + datacube shared-state path, where every spectral channel shares the lens model. `None` (the + default) leaves the standard per-fit behaviour unchanged. + """ + + self.tracer = tracer + + self.adapt_images = adapt_images + + self.settings = settings + + self.preloads = preloads + + super().__init__( + dataset=dataset, + dataset_model=dataset_model, + xp=xp, + ) + AbstractFitInversion.__init__( + self=self, model_obj=tracer, settings=settings, xp=xp + ) + + self.use_jax = xp is not np + + @property + def _xp(self): + if self.use_jax: + import jax.numpy as jnp + + return jnp + return np + + @property + def profile_visibilities(self) -> aa.Visibilities: + """ + Returns the visibilities of every light profile in the tracer, which are computed by performing a Fourier + transform to the sum of light profile images. + """ + return self.tracer.visibilities_from( + grid=self.grids.lp, transformer=self.dataset.transformer, xp=self._xp + ) + + @property + def profile_subtracted_visibilities(self) -> aa.Visibilities: + """ + Returns the interferometer dataset's visibilities with all transformed light profile images in the fit's + tracer subtracted. + """ + return self.data - self.profile_visibilities + + @property + def tracer_to_inversion(self) -> TracerToInversion: + dataset = aa.DatasetInterface( + data=self.profile_subtracted_visibilities, + noise_map=self.noise_map, + grids=self.grids, + transformer=self.dataset.transformer, + sparse_operator=self.dataset.sparse_operator, + ) + + return TracerToInversion( + dataset=dataset, + tracer=self.tracer, + adapt_images=self.adapt_images, + settings=self.settings, + xp=self._xp, + preloads=self._preloads_scoped, + ) + + @property + def _preloads_scoped(self): + """ + The preloads as consumed by this fit's inversion, scoped to its dataset type. + + Cross-dataset-type shared state (e.g. an imaging lead factor in a joint + imaging + interferometer graph) is valid here only through its source-plane mesh + geometry — a mapper / curvature matrix from another dataset type would embed that + dataset's grids and silently corrupt the fit, so non-interferometer preloads are + reduced to their mesh-geometry view. + """ + if self.preloads is None or isinstance(self.preloads, aa.PreloadsInterferometer): + return self.preloads + + return aa.PreloadsInterferometer( + source_plane_mesh_grid=self.preloads.source_plane_mesh_grid, + image_plane_mesh_grid=self.preloads.image_plane_mesh_grid, + ) + + @cached_property + def inversion(self) -> Optional[aa.AbstractInversion]: + """ + If the tracer has linear objects which are used to fit the data (e.g. a linear light profile / pixelization) + this function returns a linear inversion, where the flux values of these objects (e.g. the `intensity` + of linear light profiles) are computed via linear matrix algebra. + + The data passed to this function is the dataset's image with all light profile images of the tracer subtracted, + ensuring that the inversion only fits the data with ordinary light profiles subtracted. + """ + if self.perform_inversion: + return self.tracer_to_inversion.inversion + + @property + def model_data(self) -> aa.Visibilities: + """ + Returns the model data that is used to fit the data. + + If the tracer does not have any linear objects and therefore omits an inversion, the model data is the + sum of all light profile images Fourier transformed to visibilities. + + If a inversion is included it is the sum of these visibilities and the inversion's reconstructed visibilities. + """ + + if self.perform_inversion: + return ( + self.profile_visibilities + + self.inversion.mapped_reconstructed_operated_data + ) + + return self.profile_visibilities + + @property + def galaxy_image_dict(self) -> Dict[ag.Galaxy, np.ndarray]: + """ + A dictionary which associates every galaxy in the tracer with its `image`. + + This image is the image of the sum of: + + - The images of all ordinary light profiles in that tracer summed. + - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved + for first via the inversion. + + For modeling, this dictionary is used to set up the `adapt_images` that adapt certain pixelizations to the + data being fitted. + """ + galaxy_image_dict = self.tracer.galaxy_image_2d_dict_from( + grid=self.grids.lp, xp=self._xp + ) + + galaxy_linear_obj_image_dict = self.galaxy_linear_obj_data_dict_from( + use_operated=False + ) + + return {**galaxy_image_dict, **galaxy_linear_obj_image_dict} + + @property + def galaxy_signal_to_noise_map_dict(self) -> Dict[ag.Galaxy, np.ndarray]: + """ + A dictionary which associates every galaxy in the tracer with its signal-to-noise map. + + This signal-to-noise map is the signal-to-noise map of the sum of: + + - The images of all ordinary light profiles in that tracer summed. + - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved + for first via the inversion. + + For modeling, this dictionary is used to set up the `adapt_images` that adapt certain pixelizations to the + data being fitted. + """ + galaxy_image_dict = self.galaxy_image_dict + + galaxy_signal_to_noise_map_dict = {} + + for galaxy, image in galaxy_image_dict.items(): + galaxy_signal_to_noise_map_dict[galaxy] = image / self.dirty_noise_map + + return galaxy_signal_to_noise_map_dict + + @property + def galaxy_model_visibilities_dict(self) -> Dict[ag.Galaxy, np.ndarray]: + """ + A dictionary which associates every galaxy in the tracer with its model visibilities. + + These visibilities are the sum of: + + - The visibilities of all ordinary light profiles in that tracer summed and Fourier transformed to visibilities + space. + - The visibilities of all linear objects (e.g. linear light profiles / pixelizations), where the visibilities + are solved for first via the inversion. + """ + galaxy_model_visibilities_dict = self.tracer.galaxy_visibilities_dict_from( + grid=self.grids.lp, transformer=self.dataset.transformer, xp=self._xp + ) + + galaxy_linear_obj_visibilities_dict = self.galaxy_linear_obj_data_dict_from( + use_operated=True + ) + + return {**galaxy_model_visibilities_dict, **galaxy_linear_obj_visibilities_dict} + + @property + def model_visibilities_of_planes_list(self) -> List[aa.Visibilities]: + """ + A list of every model image of every plane in the tracer. + + This image is the image of the sum of: + + - The images of all ordinary light profiles in that plane summed and convolved with the imaging data's PSF. + - The images of all linear objects (e.g. linear light profiles / pixelizations), where the images are solved + for first via the inversion. + + This is used to visualize the different contibutions of light from the image-plane, source-plane and other + planes in a fit. + """ + galaxy_model_visibilities_dict = self.galaxy_model_visibilities_dict + + model_visibilities_of_planes_list = [ + aa.Visibilities.zeros(shape_slim=(self.dataset.data.shape_slim,)) + for i in range(self.tracer.total_planes) + ] + + for plane_index, galaxies in enumerate(self.tracer.planes): + for galaxy in galaxies: + model_visibilities_of_planes_list[ + plane_index + ] += galaxy_model_visibilities_dict[galaxy] + + return model_visibilities_of_planes_list + + @property + def tracer_linear_light_profiles_to_light_profiles(self) -> Tracer: + """ + The `Tracer` where all linear light profiles have been converted to ordinary light profiles, where their + `intensity` values are set to the values inferred by this fit. + + This is typically used for visualization, because linear light profiles cannot be used in `LightProfile` + or `Galaxy` objects. + """ + return self.model_obj_linear_light_profiles_to_light_profiles diff --git a/autolens/interferometer/model/analysis.py b/autolens/interferometer/model/analysis.py index 4fbdbb361..2dafca14d 100644 --- a/autolens/interferometer/model/analysis.py +++ b/autolens/interferometer/model/analysis.py @@ -1,378 +1,378 @@ -""" -Analysis class for fitting a ``Tracer`` lens model to an interferometer dataset. - -``AnalysisInterferometer`` implements the ``log_likelihood_function`` called by a -``PyAutoFit`` non-linear search at each iteration. It: - -1. Constructs a ``Tracer`` from the current model instance. -2. Optionally applies adaptive galaxy images to linear components. -3. Calls ``FitInterferometer`` to evaluate the log likelihood in the uv-plane. -4. Returns the figure of merit (log likelihood or log evidence). - -It also manages result output (``ResultInterferometer``), on-the-fly visualisation -(``VisualizerInterferometer``), and position-based priors via ``PositionsLH``. -""" -import logging -import numpy as np -from typing import Optional - -from autonerves.dictable import to_dict -from autonerves.fitsable import hdu_list_for_output_from - -import autofit as af -import autoarray as aa -import autogalaxy as ag - -from autolens.analysis.analysis.dataset import AnalysisDataset -from autolens.analysis.exceptions import raise_fit_exception -from autolens.analysis.positions import PositionsLH -from autolens.interferometer.model.result import ResultInterferometer -from autolens.interferometer.model.visualizer import VisualizerInterferometer -from autolens.interferometer.fit_interferometer import FitInterferometer - -logger = logging.getLogger(__name__) - -logger.setLevel(level="INFO") - - -_FIT_INTERFEROMETER_PYTREES_REGISTERED = False - - -class AnalysisInterferometer(AnalysisDataset): - Result = ResultInterferometer - Visualizer = VisualizerInterferometer - - def __init__( - self, - dataset, - positions_likelihood_list: Optional[PositionsLH] = None, - adapt_images: Optional[ag.AdaptImages] = None, - cosmology: ag.cosmo.LensingCosmology = None, - settings: aa.Settings = None, - raise_inversion_positions_likelihood_exception: bool = True, - title_prefix: str = None, - use_jax: bool = True, - shared_preloads: bool = False, - **kwargs, - ): - """ - Analysis classes are used by PyAutoFit to fit a model to a dataset via a non-linear search. - - The `Analysis` class defines the `log_likelihood_function` which fits the model to the dataset and returns the - log likelihood value defining how well the model fitted the data. - - It handles many other tasks, such as visualization, outputting results to hard-disk and storing results in - a format that can be loaded after the model-fit is complete. - - This Analysis class is used for all model-fits which fit galaxies (or objects containing galaxies like a - `Tracer`) to an interferometer dataset. - - This class stores the settings used to perform the model-fit for certain components of the model (e.g. a - pixelization or inversion), the Cosmology used for the analysis and adapt images used for certain model - classes. - - Parameters - ---------- - dataset - The interferometer dataset that the model is fitted too. - positions_likelihood_list - Alters the likelihood function to include a term which accounts for whether image-pixel coordinates in - arc-seconds corresponding to the multiple images of each lensed source galaxy trace close to one another in - their source-plane. This is a list, as it may support multiple planes, where a positions likelihood object - is input for each plane (e.g. double source plane lensing). - adapt_images - Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the - reconstructed galaxy's morphology. - cosmology - The Cosmology assumed for this analysis. - settings - Settings controlling how an inversion is fitted, for example which linear algebra formalism is used. - raise_inversion_positions_likelihood_exception - If an inversion is used without the `positions_likelihood_list` it is likely a systematic solution will - be inferred, in which case an Exception is raised before the model-fit begins to inform the user - of this. This exception is not raised if this input is False, allowing the user to perform the model-fit - anyway. - title_prefix - A string that is added before the title of all figures output by visualization, for example to - put the name of the dataset and galaxy in the title. - shared_preloads - Opts this analysis into the cross-factor shared-state mechanism of a `FactorGraphModel` (see - `shared_state_from`). Set this to `True` only when this analysis is one of many datacube channels - that share an identical lens model, so the channel-invariant inversion quantities (e.g. the - `curvature_matrix`) can be computed once and reused by every channel. `False` by default, leaving - the standard per-analysis behaviour unchanged. - """ - super().__init__( - dataset=dataset, - positions_likelihood_list=positions_likelihood_list, - adapt_images=adapt_images, - cosmology=cosmology, - settings=settings, - raise_inversion_positions_likelihood_exception=raise_inversion_positions_likelihood_exception, - title_prefix=title_prefix, - use_jax=use_jax, - **kwargs, - ) - - self.shared_preloads = shared_preloads - - @property - def interferometer(self): - return self.dataset - - def log_likelihood_function(self, instance, shared=None): - """ - Given an instance of the model, where the model parameters are set via a non-linear search, fit the model - instance to the interferometer dataset. - - This function returns a log likelihood which is used by the non-linear search to guide the model-fit. - - For this analysis class, this function performs the following steps: - - 1) If the analysis has a adapt image, associated the model galaxy images of this dataset to the galaxies in - the model instance. - - 2) Extract attributes which model aspects of the data reductions, like the scaling the background sky - and background noise. - - 3) Extracts all galaxies from the model instance and set up a `Tracer`, which includes ordering the galaxies - by redshift to set up each `Plane`. - - 4) Use the `Tracer` and other attributes to create a `FitInterferometer` object, which performs steps such as - creating model images of every galaxy in the plane, transforming them to the uv-plane via a Fourier transform - and computing residuals, a chi-squared statistic and the log likelihood. - - Certain models will fail to fit the dataset and raise an exception. For example if an `Inversion` is used, the - linear algebra calculation may be invalid and raise an Exception. In such circumstances the model is discarded - and its likelihood value is passed to the non-linear search in a way that it ignores it (for example, using a - value of -1.0e99). - - Parameters - ---------- - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - shared - The cross-factor shared state of a `FactorGraphModel`, computed once per evaluation by the lead - factor's `shared_state_from` (see that method). For this analysis it is a `PreloadsInterferometer` - carrying the channel-invariant inversion quantities; when provided it is reused by the fit instead - of being recomputed. `None` (the default, e.g. a standalone fit) leaves behaviour unchanged. - - Returns - ------- - float - The log likelihood indicating how well this model instance fitted the interferometer data. - """ - - log_likelihood_penalty = self.log_likelihood_penalty_from( - instance=instance, - ) - - if self._use_jax: - return ( - self.fit_from(instance=instance, preloads=shared).figure_of_merit - - log_likelihood_penalty - ) - - try: - return ( - self.fit_from(instance=instance, preloads=shared).figure_of_merit - - log_likelihood_penalty - ) - except Exception as e: - raise_fit_exception(e) - - def shared_state_from(self, instance: af.ModelInstance): - """ - Compute the channel-invariant inversion quantities once so they can be shared across the factors of a - datacube `FactorGraphModel` (see `autofit.Analysis.shared_state_from`). - - When `shared_preloads` is set, every factor of the graph is an interferometer channel sharing the same - lens model, so the inversion's `curvature_matrix` (`F = LᵀW̃L`) — the dominant inversion-setup cost — is - identical for every channel. This builds it once on the lead factor and returns it inside a - `PreloadsInterferometer`, which `FactorGraphModel` forwards as the `shared` argument to every factor's - `log_likelihood_function`, so each channel reuses it instead of rebuilding it. - - Returns `None` when the analysis has not opted in (`shared_preloads=False`) or when the model performs no - inversion, in which case no state is shared and every factor fits as normal. - - The caller is responsible for the invariance contract: only enable `shared_preloads` when the inversion - quantities really are channel-invariant (e.g. the narrow-emission-line regime where `uv_wavelengths` and - `noise_map` are ~channel-invariant). Outside it, leave `shared_preloads=False` so each channel computes - its own inversion. - """ - if not self.shared_preloads: - return None - - fit = self.fit_from(instance=instance) - - if not fit.perform_inversion: - return None - - # Build the inversion-setup quantities once, from a single `TracerToInversion`, so the - # mapper is built only once here: the curvature matrix `F` and the mapper (which carries - # the Delaunay triangulation) are the channel-invariant quantities reused by every factor. - tracer_to_inversion = fit.tracer_to_inversion - inversion = tracer_to_inversion.inversion - - # The mesh-geometry fields are also populated so that cross-dataset-type factors of a - # joint graph (e.g. an imaging factor when this interferometer analysis leads) can share - # the source-plane mesh; they consume ONLY these fields (see `_preloads_scoped` on the - # fits) because the mapper and curvature matrix embed this dataset's grids. - return aa.PreloadsInterferometer( - curvature_matrix=inversion.curvature_matrix, - mapper_galaxy_dict=tracer_to_inversion.mapper_galaxy_dict, - source_plane_mesh_grid=tracer_to_inversion.traced_mesh_grid_pg_list, - image_plane_mesh_grid=tracer_to_inversion.image_plane_mesh_grid_pg_list, - ) - - def fit_from( - self, instance: af.ModelInstance, preloads=None - ) -> FitInterferometer: - """ - Given a model instance create a `FitInterferometer` object. - - This function is used in the `log_likelihood_function` to fit the model to the interferometer data and compute - the log likelihood. - - Parameters - ---------- - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - preloads - An optional `PreloadsInterferometer` carrying channel-invariant inversion quantities (e.g. the - `curvature_matrix`) computed once and reused by the fit instead of being rebuilt. Supplied by the - datacube shared-state path (see `shared_state_from`); `None` (the default) fits as normal. - - Returns - ------- - FitInterferometer - The fit of the plane to the interferometer dataset, which includes the log likelihood. - """ - - if self._use_jax: - self._register_fit_interferometer_pytrees() - - tracer = self.tracer_via_instance_from( - instance=instance, - ) - - adapt_images = self.adapt_images_via_instance_from( - instance=instance, galaxies=tracer.galaxies - ) - - return FitInterferometer( - dataset=self.dataset, - tracer=tracer, - adapt_images=adapt_images, - settings=self.settings, - xp=self._xp, - preloads=preloads, - ) - - @staticmethod - def _register_fit_interferometer_pytrees() -> None: - """Register every type reachable from a ``FitInterferometer`` return - value so ``jax.jit(fit_from)`` can flatten its output. - - ``dataset``, ``adapt_images`` and ``settings`` are constants per - analysis — ride as aux so JAX does not recurse into them. Everything - else (``tracer`` and the autoarray wrappers it carries) is dynamic - per fit. - - Idempotent — guarded by the module-level - ``_FIT_INTERFEROMETER_PYTREES_REGISTERED`` flag. See - ``autolens/imaging/model/analysis.py`` for the cross-registration - rationale. - """ - global _FIT_INTERFEROMETER_PYTREES_REGISTERED - if _FIT_INTERFEROMETER_PYTREES_REGISTERED: - return - - from autoarray.abstract_ndarray import ( - register_instance_pytree, - _pytree_registered_classes, - ) - from autoarray.dataset.dataset_model import DatasetModel # fit-interferometer-pytree-mge - from autolens.lens.tracer import Tracer - - try: - from autofit.jax.pytrees import ( - _REGISTERED_INSTANCE_CLASSES as _af_registered, - ) - except ImportError: - _af_registered = set() - - for cls in (DatasetModel, Tracer): - if cls in _af_registered: - _pytree_registered_classes.add(cls) - - register_instance_pytree( - FitInterferometer, - no_flatten=("dataset", "adapt_images", "settings", "preloads"), - ) - register_instance_pytree(Tracer, no_flatten=("cosmology",)) - register_instance_pytree(DatasetModel) # fit-interferometer-pytree-mge - - _FIT_INTERFEROMETER_PYTREES_REGISTERED = True - - def save_attributes(self, paths: af.DirectoryPaths): - """ - Before the model-fit begins, this routine saves attributes of the `Analysis` object to the `files` folder - such that they can be loaded after the analysis using PyAutoFit's database and aggregator tools. - - For this analysis, it uses the `AnalysisDataset` object's method to output the following: - - - The settings associated with the inversion. - - The settings associated with the pixelization. - - The Cosmology. - - The adapt image's model image and galaxy images, as `adapt_images.fits`, if used. - - This function also outputs attributes specific to lens modeling: - - - The positions of the brightest pixels in the lensed source which are used to discard mass models. - - The following .fits files are also output via the plotter interface: - - - The real space mask applied to the dataset, in the `PrimaryHDU` of `dataset.fits`. - - The interferometer dataset as `dataset.fits` (data / noise-map / uv_wavelengths). - - It is common for these attributes to be loaded by many of the template aggregator functions given in the - `aggregator` modules. For example, when using the database tools to perform a fit, the default behaviour is for - the dataset, settings and other attributes necessary to perform the fit to be loaded via the pickle files - output by this function. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization, and the pickled objects used by the aggregator output by this function. - """ - super().save_attributes(paths=paths) - - # Output `dataset.fits` to the `files` folder so the aggregator loaders - # (e.g. `InterferometerAgg`, `agg_util.mask_header_from`) can always - # reload the dataset via `fit.value(name="dataset")`, independently of - # whether the visualization `fits_dataset` output ran. The plotter - # interface also writes this file to the `image` folder for inspection, - # but that write is gated on visualization settings and is not - # guaranteed for every fit. - paths.save_fits( - name="dataset", - fits=hdu_list_for_output_from( - values_list=[ - self.dataset.real_space_mask.astype("float"), - self.dataset.data.in_array, - self.dataset.noise_map.in_array, - self.dataset.uv_wavelengths, - ], - ext_name_list=["mask", "data", "noise_map", "uv_wavelengths"], - header_dict=self.dataset.real_space_mask.header_dict, - ), - ) - - paths.save_json( - "transformer_class", - to_dict(self.dataset.transformer.__class__), - ) +""" +Analysis class for fitting a ``Tracer`` lens model to an interferometer dataset. + +``AnalysisInterferometer`` implements the ``log_likelihood_function`` called by a +``PyAutoFit`` non-linear search at each iteration. It: + +1. Constructs a ``Tracer`` from the current model instance. +2. Optionally applies adaptive galaxy images to linear components. +3. Calls ``FitInterferometer`` to evaluate the log likelihood in the uv-plane. +4. Returns the figure of merit (log likelihood or log evidence). + +It also manages result output (``ResultInterferometer``), on-the-fly visualisation +(``VisualizerInterferometer``), and position-based priors via ``PositionsLH``. +""" +import logging +import numpy as np +from typing import Optional + +from autonerves.dictable import to_dict +from autonerves.fitsable import hdu_list_for_output_from + +import autofit as af +import autoarray as aa +import autogalaxy as ag + +from autolens.analysis.analysis.dataset import AnalysisDataset +from autolens.analysis.exceptions import raise_fit_exception +from autolens.analysis.positions import PositionsLH +from autolens.interferometer.model.result import ResultInterferometer +from autolens.interferometer.model.visualizer import VisualizerInterferometer +from autolens.interferometer.fit_interferometer import FitInterferometer + +logger = logging.getLogger(__name__) + +logger.setLevel(level="INFO") + + +_FIT_INTERFEROMETER_PYTREES_REGISTERED = False + + +class AnalysisInterferometer(AnalysisDataset): + Result = ResultInterferometer + Visualizer = VisualizerInterferometer + + def __init__( + self, + dataset, + positions_likelihood_list: Optional[PositionsLH] = None, + adapt_images: Optional[ag.AdaptImages] = None, + cosmology: ag.cosmo.LensingCosmology = None, + settings: aa.Settings = None, + raise_inversion_positions_likelihood_exception: bool = True, + title_prefix: str = None, + use_jax: bool = True, + shared_preloads: bool = False, + **kwargs, + ): + """ + Analysis classes are used by PyAutoFit to fit a model to a dataset via a non-linear search. + + The `Analysis` class defines the `log_likelihood_function` which fits the model to the dataset and returns the + log likelihood value defining how well the model fitted the data. + + It handles many other tasks, such as visualization, outputting results to hard-disk and storing results in + a format that can be loaded after the model-fit is complete. + + This Analysis class is used for all model-fits which fit galaxies (or objects containing galaxies like a + `Tracer`) to an interferometer dataset. + + This class stores the settings used to perform the model-fit for certain components of the model (e.g. a + pixelization or inversion), the Cosmology used for the analysis and adapt images used for certain model + classes. + + Parameters + ---------- + dataset + The interferometer dataset that the model is fitted too. + positions_likelihood_list + Alters the likelihood function to include a term which accounts for whether image-pixel coordinates in + arc-seconds corresponding to the multiple images of each lensed source galaxy trace close to one another in + their source-plane. This is a list, as it may support multiple planes, where a positions likelihood object + is input for each plane (e.g. double source plane lensing). + adapt_images + Contains the adapt-images which are used to make a pixelization's mesh and regularization adapt to the + reconstructed galaxy's morphology. + cosmology + The Cosmology assumed for this analysis. + settings + Settings controlling how an inversion is fitted, for example which linear algebra formalism is used. + raise_inversion_positions_likelihood_exception + If an inversion is used without the `positions_likelihood_list` it is likely a systematic solution will + be inferred, in which case an Exception is raised before the model-fit begins to inform the user + of this. This exception is not raised if this input is False, allowing the user to perform the model-fit + anyway. + title_prefix + A string that is added before the title of all figures output by visualization, for example to + put the name of the dataset and galaxy in the title. + shared_preloads + Opts this analysis into the cross-factor shared-state mechanism of a `FactorGraphModel` (see + `shared_state_from`). Set this to `True` only when this analysis is one of many datacube channels + that share an identical lens model, so the channel-invariant inversion quantities (e.g. the + `curvature_matrix`) can be computed once and reused by every channel. `False` by default, leaving + the standard per-analysis behaviour unchanged. + """ + super().__init__( + dataset=dataset, + positions_likelihood_list=positions_likelihood_list, + adapt_images=adapt_images, + cosmology=cosmology, + settings=settings, + raise_inversion_positions_likelihood_exception=raise_inversion_positions_likelihood_exception, + title_prefix=title_prefix, + use_jax=use_jax, + **kwargs, + ) + + self.shared_preloads = shared_preloads + + @property + def interferometer(self): + return self.dataset + + def log_likelihood_function(self, instance, shared=None): + """ + Given an instance of the model, where the model parameters are set via a non-linear search, fit the model + instance to the interferometer dataset. + + This function returns a log likelihood which is used by the non-linear search to guide the model-fit. + + For this analysis class, this function performs the following steps: + + 1) If the analysis has a adapt image, associated the model galaxy images of this dataset to the galaxies in + the model instance. + + 2) Extract attributes which model aspects of the data reductions, like the scaling the background sky + and background noise. + + 3) Extracts all galaxies from the model instance and set up a `Tracer`, which includes ordering the galaxies + by redshift to set up each `Plane`. + + 4) Use the `Tracer` and other attributes to create a `FitInterferometer` object, which performs steps such as + creating model images of every galaxy in the plane, transforming them to the uv-plane via a Fourier transform + and computing residuals, a chi-squared statistic and the log likelihood. + + Certain models will fail to fit the dataset and raise an exception. For example if an `Inversion` is used, the + linear algebra calculation may be invalid and raise an Exception. In such circumstances the model is discarded + and its likelihood value is passed to the non-linear search in a way that it ignores it (for example, using a + value of -1.0e99). + + Parameters + ---------- + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + shared + The cross-factor shared state of a `FactorGraphModel`, computed once per evaluation by the lead + factor's `shared_state_from` (see that method). For this analysis it is a `PreloadsInterferometer` + carrying the channel-invariant inversion quantities; when provided it is reused by the fit instead + of being recomputed. `None` (the default, e.g. a standalone fit) leaves behaviour unchanged. + + Returns + ------- + float + The log likelihood indicating how well this model instance fitted the interferometer data. + """ + + log_likelihood_penalty = self.log_likelihood_penalty_from( + instance=instance, + ) + + if self._use_jax: + return ( + self.fit_from(instance=instance, preloads=shared).figure_of_merit + - log_likelihood_penalty + ) + + try: + return ( + self.fit_from(instance=instance, preloads=shared).figure_of_merit + - log_likelihood_penalty + ) + except Exception as e: + raise_fit_exception(e) + + def shared_state_from(self, instance: af.ModelInstance): + """ + Compute the channel-invariant inversion quantities once so they can be shared across the factors of a + datacube `FactorGraphModel` (see `autofit.Analysis.shared_state_from`). + + When `shared_preloads` is set, every factor of the graph is an interferometer channel sharing the same + lens model, so the inversion's `curvature_matrix` (`F = LᵀW̃L`) — the dominant inversion-setup cost — is + identical for every channel. This builds it once on the lead factor and returns it inside a + `PreloadsInterferometer`, which `FactorGraphModel` forwards as the `shared` argument to every factor's + `log_likelihood_function`, so each channel reuses it instead of rebuilding it. + + Returns `None` when the analysis has not opted in (`shared_preloads=False`) or when the model performs no + inversion, in which case no state is shared and every factor fits as normal. + + The caller is responsible for the invariance contract: only enable `shared_preloads` when the inversion + quantities really are channel-invariant (e.g. the narrow-emission-line regime where `uv_wavelengths` and + `noise_map` are ~channel-invariant). Outside it, leave `shared_preloads=False` so each channel computes + its own inversion. + """ + if not self.shared_preloads: + return None + + fit = self.fit_from(instance=instance) + + if not fit.perform_inversion: + return None + + # Build the inversion-setup quantities once, from a single `TracerToInversion`, so the + # mapper is built only once here: the curvature matrix `F` and the mapper (which carries + # the Delaunay triangulation) are the channel-invariant quantities reused by every factor. + tracer_to_inversion = fit.tracer_to_inversion + inversion = tracer_to_inversion.inversion + + # The mesh-geometry fields are also populated so that cross-dataset-type factors of a + # joint graph (e.g. an imaging factor when this interferometer analysis leads) can share + # the source-plane mesh; they consume ONLY these fields (see `_preloads_scoped` on the + # fits) because the mapper and curvature matrix embed this dataset's grids. + return aa.PreloadsInterferometer( + curvature_matrix=inversion.curvature_matrix, + mapper_galaxy_dict=tracer_to_inversion.mapper_galaxy_dict, + source_plane_mesh_grid=tracer_to_inversion.traced_mesh_grid_pg_list, + image_plane_mesh_grid=tracer_to_inversion.image_plane_mesh_grid_pg_list, + ) + + def fit_from( + self, instance: af.ModelInstance, preloads=None + ) -> FitInterferometer: + """ + Given a model instance create a `FitInterferometer` object. + + This function is used in the `log_likelihood_function` to fit the model to the interferometer data and compute + the log likelihood. + + Parameters + ---------- + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + preloads + An optional `PreloadsInterferometer` carrying channel-invariant inversion quantities (e.g. the + `curvature_matrix`) computed once and reused by the fit instead of being rebuilt. Supplied by the + datacube shared-state path (see `shared_state_from`); `None` (the default) fits as normal. + + Returns + ------- + FitInterferometer + The fit of the plane to the interferometer dataset, which includes the log likelihood. + """ + + if self._use_jax: + self._register_fit_interferometer_pytrees() + + tracer = self.tracer_via_instance_from( + instance=instance, + ) + + adapt_images = self.adapt_images_via_instance_from( + instance=instance, galaxies=tracer.galaxies + ) + + return FitInterferometer( + dataset=self.dataset, + tracer=tracer, + adapt_images=adapt_images, + settings=self.settings, + xp=self._xp, + preloads=preloads, + ) + + @staticmethod + def _register_fit_interferometer_pytrees() -> None: + """Register every type reachable from a ``FitInterferometer`` return + value so ``jax.jit(fit_from)`` can flatten its output. + + ``dataset``, ``adapt_images`` and ``settings`` are constants per + analysis — ride as aux so JAX does not recurse into them. Everything + else (``tracer`` and the autoarray wrappers it carries) is dynamic + per fit. + + Idempotent — guarded by the module-level + ``_FIT_INTERFEROMETER_PYTREES_REGISTERED`` flag. See + ``autolens/imaging/model/analysis.py`` for the cross-registration + rationale. + """ + global _FIT_INTERFEROMETER_PYTREES_REGISTERED + if _FIT_INTERFEROMETER_PYTREES_REGISTERED: + return + + from autoarray.abstract_ndarray import ( + register_instance_pytree, + _pytree_registered_classes, + ) + from autoarray.dataset.dataset_model import DatasetModel # fit-interferometer-pytree-mge + from autolens.lens.tracer import Tracer + + try: + from autofit.jax.pytrees import ( + _REGISTERED_INSTANCE_CLASSES as _af_registered, + ) + except ImportError: + _af_registered = set() + + for cls in (DatasetModel, Tracer): + if cls in _af_registered: + _pytree_registered_classes.add(cls) + + register_instance_pytree( + FitInterferometer, + no_flatten=("dataset", "adapt_images", "settings", "preloads"), + ) + register_instance_pytree(Tracer, no_flatten=("cosmology",)) + register_instance_pytree(DatasetModel) # fit-interferometer-pytree-mge + + _FIT_INTERFEROMETER_PYTREES_REGISTERED = True + + def save_attributes(self, paths: af.DirectoryPaths): + """ + Before the model-fit begins, this routine saves attributes of the `Analysis` object to the `files` folder + such that they can be loaded after the analysis using PyAutoFit's database and aggregator tools. + + For this analysis, it uses the `AnalysisDataset` object's method to output the following: + + - The settings associated with the inversion. + - The settings associated with the pixelization. + - The Cosmology. + - The adapt image's model image and galaxy images, as `adapt_images.fits`, if used. + + This function also outputs attributes specific to lens modeling: + + - The positions of the brightest pixels in the lensed source which are used to discard mass models. + + The following .fits files are also output via the plotter interface: + + - The real space mask applied to the dataset, in the `PrimaryHDU` of `dataset.fits`. + - The interferometer dataset as `dataset.fits` (data / noise-map / uv_wavelengths). + + It is common for these attributes to be loaded by many of the template aggregator functions given in the + `aggregator` modules. For example, when using the database tools to perform a fit, the default behaviour is for + the dataset, settings and other attributes necessary to perform the fit to be loaded via the pickle files + output by this function. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization, and the pickled objects used by the aggregator output by this function. + """ + super().save_attributes(paths=paths) + + # Output `dataset.fits` to the `files` folder so the aggregator loaders + # (e.g. `InterferometerAgg`, `agg_util.mask_header_from`) can always + # reload the dataset via `fit.value(name="dataset")`, independently of + # whether the visualization `fits_dataset` output ran. The plotter + # interface also writes this file to the `image` folder for inspection, + # but that write is gated on visualization settings and is not + # guaranteed for every fit. + paths.save_fits( + name="dataset", + fits=hdu_list_for_output_from( + values_list=[ + self.dataset.real_space_mask.astype("float"), + self.dataset.data.in_array, + self.dataset.noise_map.in_array, + self.dataset.uv_wavelengths, + ], + ext_name_list=["mask", "data", "noise_map", "uv_wavelengths"], + header_dict=self.dataset.real_space_mask.header_dict, + ), + ) + + paths.save_json( + "transformer_class", + to_dict(self.dataset.transformer.__class__), + ) diff --git a/autolens/interferometer/model/result.py b/autolens/interferometer/model/result.py index 49f81dade..444f7fa5f 100644 --- a/autolens/interferometer/model/result.py +++ b/autolens/interferometer/model/result.py @@ -1,58 +1,58 @@ -import autoarray as aa - -from autogalaxy.analysis.adapt_images.adapt_images import AdaptImages -from autolens.lens.tracer import Tracer -from autolens.interferometer.fit_interferometer import FitInterferometer -from autolens.analysis.result import ResultDataset - - -class ResultInterferometer(ResultDataset): - @property - def max_log_likelihood_fit(self) -> FitInterferometer: - """ - An instance of a `FitInterferometer` corresponding to the maximum log likelihood model inferred by the - non-linear search. - """ - return self.analysis.fit_from(instance=self.instance) - - @property - def max_log_likelihood_tracer(self) -> Tracer: - """ - An instance of a `Tracer` corresponding to the maximum log likelihood model inferred by the non-linear search. - - The `Tracer` is computed from the `max_log_likelihood_fit`, as this ensures that all linear light profiles - are converted to normal light profiles with their `intensity` values updated. - """ - return ( - self.max_log_likelihood_fit.model_obj_linear_light_profiles_to_light_profiles - ) - - @property - def real_space_mask(self) -> aa.Mask2D: - """ - The real space mask used by this model-fit. - """ - return self.max_log_likelihood_fit.dataset.real_space_mask - - def adapt_images_from(self, use_model_images: bool = False) -> AdaptImages: - """ - Returns the adapt-images which are used to make a pixelization's mesh and regularization adapt to the - reconstructed galaxy's morphology. - - This can use either: - - - The model image of each galaxy in the best-fit model. - - The subtracted image of each galaxy in the best-fit model, where the subtracted image is the dataset - minus the model images of all other galaxies. - - In **PyAutoLens** these adapt images have had lensing calculations performed on them and therefore for source - galaxies are their lensed model images in the image-plane. - - Parameters - ---------- - use_model_images - If True, the model images of the galaxies are used to create the adapt images. If False, the subtracted - images of the galaxies are used. - """ - - return AdaptImages.from_result(result=self, use_model_images=True) +import autoarray as aa + +from autogalaxy.analysis.adapt_images.adapt_images import AdaptImages +from autolens.lens.tracer import Tracer +from autolens.interferometer.fit_interferometer import FitInterferometer +from autolens.analysis.result import ResultDataset + + +class ResultInterferometer(ResultDataset): + @property + def max_log_likelihood_fit(self) -> FitInterferometer: + """ + An instance of a `FitInterferometer` corresponding to the maximum log likelihood model inferred by the + non-linear search. + """ + return self.analysis.fit_from(instance=self.instance) + + @property + def max_log_likelihood_tracer(self) -> Tracer: + """ + An instance of a `Tracer` corresponding to the maximum log likelihood model inferred by the non-linear search. + + The `Tracer` is computed from the `max_log_likelihood_fit`, as this ensures that all linear light profiles + are converted to normal light profiles with their `intensity` values updated. + """ + return ( + self.max_log_likelihood_fit.model_obj_linear_light_profiles_to_light_profiles + ) + + @property + def real_space_mask(self) -> aa.Mask2D: + """ + The real space mask used by this model-fit. + """ + return self.max_log_likelihood_fit.dataset.real_space_mask + + def adapt_images_from(self, use_model_images: bool = False) -> AdaptImages: + """ + Returns the adapt-images which are used to make a pixelization's mesh and regularization adapt to the + reconstructed galaxy's morphology. + + This can use either: + + - The model image of each galaxy in the best-fit model. + - The subtracted image of each galaxy in the best-fit model, where the subtracted image is the dataset + minus the model images of all other galaxies. + + In **PyAutoLens** these adapt images have had lensing calculations performed on them and therefore for source + galaxies are their lensed model images in the image-plane. + + Parameters + ---------- + use_model_images + If True, the model images of the galaxies are used to create the adapt images. If False, the subtracted + images of the galaxies are used. + """ + + return AdaptImages.from_result(result=self, use_model_images=True) diff --git a/autolens/interferometer/model/visualizer.py b/autolens/interferometer/model/visualizer.py index d70883d3c..39ecf3a2f 100644 --- a/autolens/interferometer/model/visualizer.py +++ b/autolens/interferometer/model/visualizer.py @@ -1,231 +1,231 @@ -import logging - -import autofit as af -import autogalaxy as ag - -from autolens.interferometer.model.plotter import ( - PlotterInterferometer, -) -from autolens.interferometer.plot.fit_interferometer_plots import _compute_critical_curve_lines -from autogalaxy import exc - -logger = logging.getLogger(__name__) - - -class VisualizerInterferometer(af.Visualizer): - @staticmethod - def visualize_before_fit( - analysis, - paths: af.AbstractPaths, - model: af.AbstractPriorModel, - ): - """ - PyAutoFit calls this function immediately before the non-linear search begins. - - It visualizes objects which do not change throughout the model fit like the dataset. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization and the pickled objects used by the aggregator output by this function. - model - The model object, which includes model components representing the galaxies that are fitted to - the imaging data. - """ - - plotter = PlotterInterferometer( - image_path=paths.image_path, title_prefix=analysis.title_prefix - ) - - plotter.interferometer(dataset=analysis.interferometer) - - if analysis.positions_likelihood_list is not None: - - positions_list = [] - - for positions_likelihood in analysis.positions_likelihood_list: - positions_list.append(positions_likelihood.positions) - - positions = ag.Grid2DIrregular(positions_list) - - plotter.image_with_positions( - image=analysis.dataset.dirty_image, positions=positions - ) - - if analysis.adapt_images is not None: - plotter.adapt_images(adapt_images=analysis.adapt_images) - - @staticmethod - def visualize( - analysis, - paths: af.DirectoryPaths, - instance: af.ModelInstance, - during_analysis: bool, - quick_update: bool = False, - ): - """ - Outputs images of the maximum log likelihood model inferred by the model-fit. This function is called - throughout the non-linear search at input intervals, and therefore provides on-the-fly visualization of how - well the model-fit is going. - - The visualization performed by this function includes: - - - Images of the best-fit `Tracer`, including the images of each of its galaxies. - - - Images of the best-fit `FitInterferometer`, including the model-image, residuals and chi-squared of its fit - to the imaging data. - - - The adapt-images of the model-fit showing how the galaxies are used to represent different galaxies in - the dataset. - - - If adapt features are used to scale the noise, a `FitInterferometer` with these features turned off may be - output, to indicate how much these features are altering the dataset. - - The images output by this function are customized using the file `config/visualize/plots.yaml`. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization, and the pickled objects used by the aggregator output by this function. - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - """ - fit = analysis.fit_for_visualization(instance=instance) - tracer = fit.tracer_linear_light_profiles_to_light_profiles - - plotter = PlotterInterferometer( - image_path=paths.image_path, title_prefix=analysis.title_prefix - ) - - # Compute grid and critical curves once for all plot functions. - grid = fit.dataset.real_space_mask.derive_grid.all_false - ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curve_lines( - tracer, grid - ) - - try: - plotter.fit_interferometer( - fit=fit, - quick_update=quick_update, - image_plane_lines=ip_lines, - image_plane_line_colors=ip_colors, - source_plane_lines=sp_lines, - source_plane_line_colors=sp_colors, - ) - except exc.InversionException: - logger(ag.exc.invalid_linear_algebra_for_visualization_message()) - return - - if quick_update: - return - - if analysis.positions_likelihood_list is not None: - - overwrite_file = True - - for positions_likelihood in analysis.positions_likelihood_list: - - positions_likelihood.output_positions_info( - output_path=paths.output_path, - tracer=fit.tracer, - overwrite_file=overwrite_file, - ) - - overwrite_file = False - - if fit.inversion is not None: - try: - fit.inversion.reconstruction - except exc.InversionException: - return - - plotter.tracer( - tracer=tracer, - grid=grid, - image_plane_lines=ip_lines, - image_plane_line_colors=ip_colors, - source_plane_lines=sp_lines, - source_plane_line_colors=sp_colors, - ) - plotter.galaxies( - galaxies=tracer.galaxies, - grid=fit.grids.lp, - ) - if fit.inversion is not None: - try: - plotter.inversion( - inversion=fit.inversion, - ) - except IndexError: - pass - - @staticmethod - def visualize_combined( - analyses, - paths: af.AbstractPaths, - instance: af.ModelInstance, - during_analysis: bool, - quick_update: bool = False, - ): - """ - Performs visualization during the non-linear search of information that is - shared across all per-channel interferometer analyses, on a single multi-row - figure. Used for ALMA-style datacube fits where each channel is its own - ``Interferometer`` dataset wrapped in an ``af.AnalysisFactor``. - - Outputs ``fit_combined.png``: a row-per-channel subplot showing dirty image, - dirty model image, source-plane reconstruction and dirty normalised residual - map. The plot makes it easy to see how an emission line's source-plane - morphology shifts across the cube while the lens model stays fixed. - - Parameters - ---------- - analyses - The list of all per-channel ``AnalysisInterferometer`` objects. - paths - The paths object which manages where visualisation is written to. - instance - A ``Collection`` of per-factor model instances. Iterating it yields one - ``ModelInstance`` per channel, in the same order as ``analyses``. - during_analysis - ``True`` when called during the non-linear search, ``False`` when - called after the search completes. - quick_update - ``True`` when called from the search's quick-update hook between - iterations; only the headline combined plot is written in that case. - """ - - if analyses is None: - return - - # A mixed-dataset factor graph (e.g. imaging + weak lensing) routes every - # factor's analysis here; this combined subplot can only draw its own - # dataset type, so other analyses are skipped (each still visualizes its - # own fit individually). - from autolens.interferometer.model.analysis import AnalysisInterferometer - - pairs = [ - (analysis, single_instance) - for analysis, single_instance in zip(analyses, instance) - if isinstance(analysis, AnalysisInterferometer) - ] - - if len(pairs) == 0: - return - - plotter = PlotterInterferometer( - image_path=paths.image_path, title_prefix=pairs[0][0].title_prefix - ) - - fit_list = [ - analysis.fit_for_visualization(instance=single_instance) - for analysis, single_instance in pairs - ] - - plotter.fit_interferometer_combined( - fit_list=fit_list, - quick_update=quick_update, - ) +import logging + +import autofit as af +import autogalaxy as ag + +from autolens.interferometer.model.plotter import ( + PlotterInterferometer, +) +from autolens.interferometer.plot.fit_interferometer_plots import _compute_critical_curve_lines +from autogalaxy import exc + +logger = logging.getLogger(__name__) + + +class VisualizerInterferometer(af.Visualizer): + @staticmethod + def visualize_before_fit( + analysis, + paths: af.AbstractPaths, + model: af.AbstractPriorModel, + ): + """ + PyAutoFit calls this function immediately before the non-linear search begins. + + It visualizes objects which do not change throughout the model fit like the dataset. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + model + The model object, which includes model components representing the galaxies that are fitted to + the imaging data. + """ + + plotter = PlotterInterferometer( + image_path=paths.image_path, title_prefix=analysis.title_prefix + ) + + plotter.interferometer(dataset=analysis.interferometer) + + if analysis.positions_likelihood_list is not None: + + positions_list = [] + + for positions_likelihood in analysis.positions_likelihood_list: + positions_list.append(positions_likelihood.positions) + + positions = ag.Grid2DIrregular(positions_list) + + plotter.image_with_positions( + image=analysis.dataset.dirty_image, positions=positions + ) + + if analysis.adapt_images is not None: + plotter.adapt_images(adapt_images=analysis.adapt_images) + + @staticmethod + def visualize( + analysis, + paths: af.DirectoryPaths, + instance: af.ModelInstance, + during_analysis: bool, + quick_update: bool = False, + ): + """ + Outputs images of the maximum log likelihood model inferred by the model-fit. This function is called + throughout the non-linear search at input intervals, and therefore provides on-the-fly visualization of how + well the model-fit is going. + + The visualization performed by this function includes: + + - Images of the best-fit `Tracer`, including the images of each of its galaxies. + + - Images of the best-fit `FitInterferometer`, including the model-image, residuals and chi-squared of its fit + to the imaging data. + + - The adapt-images of the model-fit showing how the galaxies are used to represent different galaxies in + the dataset. + + - If adapt features are used to scale the noise, a `FitInterferometer` with these features turned off may be + output, to indicate how much these features are altering the dataset. + + The images output by this function are customized using the file `config/visualize/plots.yaml`. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization, and the pickled objects used by the aggregator output by this function. + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + """ + fit = analysis.fit_for_visualization(instance=instance) + tracer = fit.tracer_linear_light_profiles_to_light_profiles + + plotter = PlotterInterferometer( + image_path=paths.image_path, title_prefix=analysis.title_prefix + ) + + # Compute grid and critical curves once for all plot functions. + grid = fit.dataset.real_space_mask.derive_grid.all_false + ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curve_lines( + tracer, grid + ) + + try: + plotter.fit_interferometer( + fit=fit, + quick_update=quick_update, + image_plane_lines=ip_lines, + image_plane_line_colors=ip_colors, + source_plane_lines=sp_lines, + source_plane_line_colors=sp_colors, + ) + except exc.InversionException: + logger(ag.exc.invalid_linear_algebra_for_visualization_message()) + return + + if quick_update: + return + + if analysis.positions_likelihood_list is not None: + + overwrite_file = True + + for positions_likelihood in analysis.positions_likelihood_list: + + positions_likelihood.output_positions_info( + output_path=paths.output_path, + tracer=fit.tracer, + overwrite_file=overwrite_file, + ) + + overwrite_file = False + + if fit.inversion is not None: + try: + fit.inversion.reconstruction + except exc.InversionException: + return + + plotter.tracer( + tracer=tracer, + grid=grid, + image_plane_lines=ip_lines, + image_plane_line_colors=ip_colors, + source_plane_lines=sp_lines, + source_plane_line_colors=sp_colors, + ) + plotter.galaxies( + galaxies=tracer.galaxies, + grid=fit.grids.lp, + ) + if fit.inversion is not None: + try: + plotter.inversion( + inversion=fit.inversion, + ) + except IndexError: + pass + + @staticmethod + def visualize_combined( + analyses, + paths: af.AbstractPaths, + instance: af.ModelInstance, + during_analysis: bool, + quick_update: bool = False, + ): + """ + Performs visualization during the non-linear search of information that is + shared across all per-channel interferometer analyses, on a single multi-row + figure. Used for ALMA-style datacube fits where each channel is its own + ``Interferometer`` dataset wrapped in an ``af.AnalysisFactor``. + + Outputs ``fit_combined.png``: a row-per-channel subplot showing dirty image, + dirty model image, source-plane reconstruction and dirty normalised residual + map. The plot makes it easy to see how an emission line's source-plane + morphology shifts across the cube while the lens model stays fixed. + + Parameters + ---------- + analyses + The list of all per-channel ``AnalysisInterferometer`` objects. + paths + The paths object which manages where visualisation is written to. + instance + A ``Collection`` of per-factor model instances. Iterating it yields one + ``ModelInstance`` per channel, in the same order as ``analyses``. + during_analysis + ``True`` when called during the non-linear search, ``False`` when + called after the search completes. + quick_update + ``True`` when called from the search's quick-update hook between + iterations; only the headline combined plot is written in that case. + """ + + if analyses is None: + return + + # A mixed-dataset factor graph (e.g. imaging + weak lensing) routes every + # factor's analysis here; this combined subplot can only draw its own + # dataset type, so other analyses are skipped (each still visualizes its + # own fit individually). + from autolens.interferometer.model.analysis import AnalysisInterferometer + + pairs = [ + (analysis, single_instance) + for analysis, single_instance in zip(analyses, instance) + if isinstance(analysis, AnalysisInterferometer) + ] + + if len(pairs) == 0: + return + + plotter = PlotterInterferometer( + image_path=paths.image_path, title_prefix=pairs[0][0].title_prefix + ) + + fit_list = [ + analysis.fit_for_visualization(instance=single_instance) + for analysis, single_instance in pairs + ] + + plotter.fit_interferometer_combined( + fit_list=fit_list, + quick_update=quick_update, + ) diff --git a/autolens/interferometer/simulator.py b/autolens/interferometer/simulator.py index d86367811..e7d36c834 100644 --- a/autolens/interferometer/simulator.py +++ b/autolens/interferometer/simulator.py @@ -1,152 +1,152 @@ -""" -Interferometer simulator for strong gravitational lens observations. - -``SimulatorInterferometer`` extends ``aa.SimulatorInterferometer`` with a -``via_tracer_from`` method that accepts a ``Tracer`` object and: - -1. Uses the tracer's mass profiles to ray-trace the real-space grid to the source plane. -2. Evaluates the light profiles of all galaxies on the (traced) grid. -3. Applies the Fourier transform to map the real-space image to complex visibilities. -4. Adds noise to the visibilities according to the noise-map stored in the simulator. - -This is the primary entry point for generating synthetic interferometer datasets for -testing, validation, and mock-data studies of ALMA or JVLA observations. -""" -from typing import List - -import autoarray as aa -import autogalaxy as ag - -from autolens.lens.tracer import Tracer - - -class SimulatorInterferometer(aa.SimulatorInterferometer): - def via_tracer_from(self, tracer, grid, xp=None): - """ - Returns a realistic simulated image by applying effects to a plain simulated image. - - Parameters - ---------- - image - The image before simulating (e.g. the lens and source galaxies before optics blurring and Imaging read-out). - pixel_scales - The scale of each pixel in arc seconds - exposure_time_map - An arrays representing the effective exposure time of each pixel. - psf: PSF - An arrays describing the PSF the simulated image is blurred with. - add_poisson_noise_to_data: Bool - If `True` poisson noise_maps is simulated and added to the image, based on the total counts in each image - pixel - noise_seed: int - A seed for random noise_maps generation - """ - - if xp is None: - xp = self._xp - - image = tracer.image_2d_from(grid=grid, xp=xp) - - return self.via_image_from(image=image, xp=xp) - - def via_galaxies_from(self, galaxies, grid, xp=None): - """Simulate imaging data for this data, as follows: - - 1) Setup the image-plane grid of the Imaging arrays, which defines the coordinates used for the ray-tracing. - - 2) Use this grid and the lens and source galaxies to setup a tracer, which generates the image of \ - the simulated imaging data. - - 3) Simulate the imaging data, using a special image which ensures edge-effects don't - degrade simulator of the telescope optics (e.g. the PSF convolution). - - 4) Plot the image using Matplotlib, if the plot_imaging bool is True. - - 5) Output the dataset to .fits format if a dataset_path and data_name are specified. Otherwise, return the simulated \ - imaging data instance.""" - - tracer = Tracer(galaxies=galaxies) - - return self.via_tracer_from(tracer=tracer, grid=grid, xp=xp) - - def via_deflections_and_galaxies_from( - self, deflections: aa.VectorYX2D, galaxies: List[ag.Galaxy] - ) -> aa.Imaging: - """ - Simulate an `Imaging` dataset from an input deflection angle map and list of galaxies. - - The input deflection angle map ray-traces the image-plane coordinates from the image-plane to source-plane, - via the lens equation. - - This traced grid is then used to evaluate the light of the list of galaxies, which therefore simulate the - image of the strong lens. - - This function is used in situations where one has access to a deflection angle map which does not suit being - ray-traced using a `Tracer` object (e.g. deflection angles from a cosmological simulation of a galaxy). - - The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are - described in the `SimulatorImaging` `__init__` method docstring. - - Parameters - ---------- - galaxies - The galaxies used to create the tracer, which describes the ray-tracing and strong lens configuration - used to simulate the imaging dataset. - grid - The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. - """ - grid = aa.Grid2D.uniform( - shape_native=deflections.shape_native, - pixel_scales=deflections.pixel_scales, - over_sample_size=1, - ) - - deflected_grid = aa.Grid2D( - values=grid - deflections, - mask=grid.mask, - over_sample_size=1, - over_sampled=grid - deflections, - over_sampler=grid.over_sampler, - ) - - image = sum(map(lambda g: g.image_2d_from(grid=deflected_grid), galaxies)) - - return self.via_image_from(image=image) - - def via_source_image_from( - self, tracer: Tracer, grid: aa.type.Grid2DLike, source_image: aa.Array2D - ) -> aa.Imaging: - """ - Simulate an `Interferometer` dataset from an input image of a source galaxy. - - This input image is on a uniform and regular 2D array, meaning it can simulate the source's irregular - and asymmetric source galaxy morphological features. - - The typical use case is inputting the image of an irregular galaxy in the source-plane (whose values are - on a uniform array) and using this function to compute the lensed image of this source galaxy. - - The tracer is used to perform ray-tracing and generate the image of the strong lens galaxies (e.g. - the lens light, lensed source light, etc) which is simulated. - - The source galaxy light profiles are ignored in favour of the input source image, but the emission of - other galaxies (e.g. the lems galaxy's light) are included. - - The steps of the `SimulatorInterferometer` simulation process (e.g. PSF convolution, noise addition) are - described in the `SimulatorInterferometer` `__init__` method docstring. - - Parameters - ---------- - tracer - The tracer, which describes the ray-tracing and strong lens configuration used to simulate the - Interferometer dataset. - grid - The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. - source_image - The image of the source-plane and source galaxy which is interpolated to compute the lensed image. - """ - - image = tracer.image_2d_via_input_plane_image_from( - grid=grid, plane_image=source_image - ) - - return self.via_image_from(image=image) +""" +Interferometer simulator for strong gravitational lens observations. + +``SimulatorInterferometer`` extends ``aa.SimulatorInterferometer`` with a +``via_tracer_from`` method that accepts a ``Tracer`` object and: + +1. Uses the tracer's mass profiles to ray-trace the real-space grid to the source plane. +2. Evaluates the light profiles of all galaxies on the (traced) grid. +3. Applies the Fourier transform to map the real-space image to complex visibilities. +4. Adds noise to the visibilities according to the noise-map stored in the simulator. + +This is the primary entry point for generating synthetic interferometer datasets for +testing, validation, and mock-data studies of ALMA or JVLA observations. +""" +from typing import List + +import autoarray as aa +import autogalaxy as ag + +from autolens.lens.tracer import Tracer + + +class SimulatorInterferometer(aa.SimulatorInterferometer): + def via_tracer_from(self, tracer, grid, xp=None): + """ + Returns a realistic simulated image by applying effects to a plain simulated image. + + Parameters + ---------- + image + The image before simulating (e.g. the lens and source galaxies before optics blurring and Imaging read-out). + pixel_scales + The scale of each pixel in arc seconds + exposure_time_map + An arrays representing the effective exposure time of each pixel. + psf: PSF + An arrays describing the PSF the simulated image is blurred with. + add_poisson_noise_to_data: Bool + If `True` poisson noise_maps is simulated and added to the image, based on the total counts in each image + pixel + noise_seed: int + A seed for random noise_maps generation + """ + + if xp is None: + xp = self._xp + + image = tracer.image_2d_from(grid=grid, xp=xp) + + return self.via_image_from(image=image, xp=xp) + + def via_galaxies_from(self, galaxies, grid, xp=None): + """Simulate imaging data for this data, as follows: + + 1) Setup the image-plane grid of the Imaging arrays, which defines the coordinates used for the ray-tracing. + + 2) Use this grid and the lens and source galaxies to setup a tracer, which generates the image of \ + the simulated imaging data. + + 3) Simulate the imaging data, using a special image which ensures edge-effects don't + degrade simulator of the telescope optics (e.g. the PSF convolution). + + 4) Plot the image using Matplotlib, if the plot_imaging bool is True. + + 5) Output the dataset to .fits format if a dataset_path and data_name are specified. Otherwise, return the simulated \ + imaging data instance.""" + + tracer = Tracer(galaxies=galaxies) + + return self.via_tracer_from(tracer=tracer, grid=grid, xp=xp) + + def via_deflections_and_galaxies_from( + self, deflections: aa.VectorYX2D, galaxies: List[ag.Galaxy] + ) -> aa.Imaging: + """ + Simulate an `Imaging` dataset from an input deflection angle map and list of galaxies. + + The input deflection angle map ray-traces the image-plane coordinates from the image-plane to source-plane, + via the lens equation. + + This traced grid is then used to evaluate the light of the list of galaxies, which therefore simulate the + image of the strong lens. + + This function is used in situations where one has access to a deflection angle map which does not suit being + ray-traced using a `Tracer` object (e.g. deflection angles from a cosmological simulation of a galaxy). + + The steps of the `SimulatorImaging` simulation process (e.g. PSF convolution, noise addition) are + described in the `SimulatorImaging` `__init__` method docstring. + + Parameters + ---------- + galaxies + The galaxies used to create the tracer, which describes the ray-tracing and strong lens configuration + used to simulate the imaging dataset. + grid + The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. + """ + grid = aa.Grid2D.uniform( + shape_native=deflections.shape_native, + pixel_scales=deflections.pixel_scales, + over_sample_size=1, + ) + + deflected_grid = aa.Grid2D( + values=grid - deflections, + mask=grid.mask, + over_sample_size=1, + over_sampled=grid - deflections, + over_sampler=grid.over_sampler, + ) + + image = sum(map(lambda g: g.image_2d_from(grid=deflected_grid), galaxies)) + + return self.via_image_from(image=image) + + def via_source_image_from( + self, tracer: Tracer, grid: aa.type.Grid2DLike, source_image: aa.Array2D + ) -> aa.Imaging: + """ + Simulate an `Interferometer` dataset from an input image of a source galaxy. + + This input image is on a uniform and regular 2D array, meaning it can simulate the source's irregular + and asymmetric source galaxy morphological features. + + The typical use case is inputting the image of an irregular galaxy in the source-plane (whose values are + on a uniform array) and using this function to compute the lensed image of this source galaxy. + + The tracer is used to perform ray-tracing and generate the image of the strong lens galaxies (e.g. + the lens light, lensed source light, etc) which is simulated. + + The source galaxy light profiles are ignored in favour of the input source image, but the emission of + other galaxies (e.g. the lems galaxy's light) are included. + + The steps of the `SimulatorInterferometer` simulation process (e.g. PSF convolution, noise addition) are + described in the `SimulatorInterferometer` `__init__` method docstring. + + Parameters + ---------- + tracer + The tracer, which describes the ray-tracing and strong lens configuration used to simulate the + Interferometer dataset. + grid + The image-plane 2D grid of (y,x) coordinates grid which the image of the strong lens is generated on. + source_image + The image of the source-plane and source galaxy which is interpolated to compute the lensed image. + """ + + image = tracer.image_2d_via_input_plane_image_from( + grid=grid, plane_image=source_image + ) + + return self.via_image_from(image=image) diff --git a/autolens/lens/mock/mock_to_inversion.py b/autolens/lens/mock/mock_to_inversion.py index 8ed180c81..aa423da13 100644 --- a/autolens/lens/mock/mock_to_inversion.py +++ b/autolens/lens/mock/mock_to_inversion.py @@ -1,15 +1,15 @@ -from typing import Dict, Optional - - -class MockTracerToInversion: - def __init__( - self, - tracer, - image_plane_mesh_grid_pg_list=None, - ): - self.tracer = tracer - - self.image_plane_mesh_grid_pg_list = image_plane_mesh_grid_pg_list - - def image_plane_mesh_grid_pg_list(self): - return self.image_plane_mesh_grid_pg_list +from typing import Dict, Optional + + +class MockTracerToInversion: + def __init__( + self, + tracer, + image_plane_mesh_grid_pg_list=None, + ): + self.tracer = tracer + + self.image_plane_mesh_grid_pg_list = image_plane_mesh_grid_pg_list + + def image_plane_mesh_grid_pg_list(self): + return self.image_plane_mesh_grid_pg_list diff --git a/autolens/lens/mock/mock_tracer.py b/autolens/lens/mock/mock_tracer.py index 7cdc4c2f7..7fb5b0138 100644 --- a/autolens/lens/mock/mock_tracer.py +++ b/autolens/lens/mock/mock_tracer.py @@ -1,65 +1,65 @@ -import numpy as np - - -class MockTracer: - def __init__( - self, traced_grid_2d_list_from=None, image_plane_mesh_grid_pg_list=None - ): - self.image_plane_mesh_grid_pg_list = image_plane_mesh_grid_pg_list - self._traced_grid_2d_list_from = traced_grid_2d_list_from - - def traced_grid_2d_list_from(self, grid, xp=np): - return self._traced_grid_2d_list_from - - def plane_index_via_redshift_from(self, redshift): - raise TypeError - - -class MockTracerPoint(MockTracer): - def __init__( - self, - image_plane_mesh_grid_pg_list=None, - traced_grid=None, - attribute=None, - profile=None, - magnification=None, - einstein_radius=None, - einstein_mass=None, - time_delays=None, - ): - super().__init__(image_plane_mesh_grid_pg_list=image_plane_mesh_grid_pg_list) - - self.positions = traced_grid - - self.attribute = attribute - self.profile = profile - - self.magnification = magnification - self.einstein_radius = einstein_radius - self.einstein_mass = einstein_mass - self.time_delays = time_delays - - @property - def planes(self): - return [0, 1] - - def deflections_yx_2d_from(self, grid, xp=np): - return xp.zeros_like(grid.array) - - def extract_attribute(self, cls, attr_name): - return [self.attribute] - - def extract_profile(self, profile_name): - return self.profile - - def traced_grid_2d_list_from(self, grid, plane_index_limit=None): - return [self.positions] - - def einstein_radius_from(self, grid): - return self.einstein_radius - - def einstein_mass_angular_from(self, grid): - return self.einstein_mass - - def time_delays_from(self, grid, xp=np): - return self.time_delays +import numpy as np + + +class MockTracer: + def __init__( + self, traced_grid_2d_list_from=None, image_plane_mesh_grid_pg_list=None + ): + self.image_plane_mesh_grid_pg_list = image_plane_mesh_grid_pg_list + self._traced_grid_2d_list_from = traced_grid_2d_list_from + + def traced_grid_2d_list_from(self, grid, xp=np): + return self._traced_grid_2d_list_from + + def plane_index_via_redshift_from(self, redshift): + raise TypeError + + +class MockTracerPoint(MockTracer): + def __init__( + self, + image_plane_mesh_grid_pg_list=None, + traced_grid=None, + attribute=None, + profile=None, + magnification=None, + einstein_radius=None, + einstein_mass=None, + time_delays=None, + ): + super().__init__(image_plane_mesh_grid_pg_list=image_plane_mesh_grid_pg_list) + + self.positions = traced_grid + + self.attribute = attribute + self.profile = profile + + self.magnification = magnification + self.einstein_radius = einstein_radius + self.einstein_mass = einstein_mass + self.time_delays = time_delays + + @property + def planes(self): + return [0, 1] + + def deflections_yx_2d_from(self, grid, xp=np): + return xp.zeros_like(grid.array) + + def extract_attribute(self, cls, attr_name): + return [self.attribute] + + def extract_profile(self, profile_name): + return self.profile + + def traced_grid_2d_list_from(self, grid, plane_index_limit=None): + return [self.positions] + + def einstein_radius_from(self, grid): + return self.einstein_radius + + def einstein_mass_angular_from(self, grid): + return self.einstein_mass + + def time_delays_from(self, grid, xp=np): + return self.time_delays diff --git a/autolens/lens/sensitivity.py b/autolens/lens/sensitivity.py index 549174b6a..a7bff2ada 100644 --- a/autolens/lens/sensitivity.py +++ b/autolens/lens/sensitivity.py @@ -1,148 +1,148 @@ -""" -Sensitivity mapping for dark-matter subhalo detection. - -Sensitivity mapping asks: *given a lens model, how detectable would a dark-matter subhalo -of mass M at position (y, x) be?* It works by: - -1. Fixing the smooth lens model to the best-fit result. -2. Simulating many lens datasets, each with a subhalo at a different position and mass, - using the smooth model plus a perturbation. -3. Fitting each simulated dataset with the smooth model (no subhalo) to measure the change - in log-evidence caused by the subhalo. - -``SubhaloSensitivityResult`` wraps the generic ``PyAutoFit`` ``SensitivityResult`` with -convenience properties for the subhalo grid positions (``y``, ``x``), the detection -significance map, and Matplotlib visualisation helpers. -""" -import numpy as np -from typing import Optional, List, Tuple - -from autofit.non_linear.grid.sensitivity.result import SensitivityResult - -import autofit as af -import autoarray as aa - -from autolens.lens.tracer import Tracer - - -class SubhaloSensitivityResult(SensitivityResult): - def __init__( - self, - result: SensitivityResult, - ): - """ - The results of a subhalo sensitivity mapping analysis, where dark matter halos are used to simulate many - strong lens datasets which are fitted to quantify how detectable they are. - - Parameters - ---------- - result - The results of a sensitivity mapping analysis where. - """ - - super().__init__( - samples=result.samples, - perturb_samples=result.perturb_samples, - shape=result.shape, - path_values=result.path_values, - ) - - @property - def y(self) -> af.GridList: - """ - The y coordinates of the physical values of the sensitivity mapping grid. - - These are the `centre` coordinates of the dark matter subhalos that are included in the simulated datasets. - """ - return self.perturbed_physical_centres_list_from(path="mass.centre.centre_0") - - @property - def x(self) -> af.GridList: - """ - The x coordinates of the physical values of the sensitivity mapping grid. - - These are the `centre` coordinates of the dark matter subhalos that are included in the simulated datasets. - """ - return self.perturbed_physical_centres_list_from(path="mass.centre.centre_1") - - @property - def extent(self) -> Tuple[float, float, float, float]: - """ - The extent of the sensitivity mapping grid, which is the minimum and maximum values of the x and y coordinates. - """ - return (np.min(self.x), np.max(self.x), np.min(self.y), np.max(self.y)) - - def _array_2d_from(self, values) -> aa.Array2D: - """ - Returns an `Array2D` where the input values are reshaped from list of lists to a 2D array, which is - suitable for plotting. - - For example, this function may return the 2D array of the increases in log evidence for every lens model - fitted with a DM subhalo in the sensitivity mapping compared to the model without a DM subhalo. - - The orientation of the 2D array and its values are chosen to ensure that when this array is plotted, DM - subhalos with positive y and negative x `centre` coordinates appear in the top-left of the image. - - Parameters - ---------- - values_native - The list of list of values which are mapped to the 2D array (e.g. the `log_evidence` difference of every - lens model with a DM subhalo compared to the one without). - - Returns - ------- - The 2D array of values, where the values are mapped from the input list of lists. - """ - values_reshaped = [value for values in values.native for value in values] - - pixel_scale_list = [] - - for i in range(len(values_reshaped) - 1): - pixel_scale = abs(self.x[i] - self.x[i + 1]) - if pixel_scale > 0.0: - pixel_scale_list.append(pixel_scale) - - pixel_scales = np.min(pixel_scale_list) - - return aa.Array2D.from_yx_and_values( - y=self.y, - x=self.x, - values=values_reshaped, - pixel_scales=(pixel_scales, pixel_scales), - shape_native=self.shape, - ) - - def figure_of_merit_array( - self, - use_log_evidences: bool = True, - remove_zeros: bool = False, - ) -> aa.Array2D: - """ - Returns an `Array2D` where the values are the figure of merit (`log_evidence` or `log_likelihood` difference) - of every lens model on the sensitivity mapping grid. - - Values below zero may be rounded to zero, to prevent the figure of merit map being dominated by low values - - Parameters - ---------- - use_log_evidences - If `True`, the figure of merit values are the log evidences of every lens model on the grid search. - If `False`, they are the log likelihoods. - remove_zeros - If `True`, the figure of merit array is altered so that all values below 0.0 and set to 0.0. For plotting - relative figures of merit for Bayesian model comparison, this is convenient to remove negative values - and produce a clearer visualization of the overlay. - """ - - figures_of_merits = self.figure_of_merits( - use_log_evidences=use_log_evidences, - ) - - if remove_zeros: - figures_of_merits = af.GridList( - values=[fom if fom > 0.0 else 0.0 for fom in figures_of_merits], - shape=figures_of_merits.shape, - ) - - return self._array_2d_from(values=figures_of_merits) - +""" +Sensitivity mapping for dark-matter subhalo detection. + +Sensitivity mapping asks: *given a lens model, how detectable would a dark-matter subhalo +of mass M at position (y, x) be?* It works by: + +1. Fixing the smooth lens model to the best-fit result. +2. Simulating many lens datasets, each with a subhalo at a different position and mass, + using the smooth model plus a perturbation. +3. Fitting each simulated dataset with the smooth model (no subhalo) to measure the change + in log-evidence caused by the subhalo. + +``SubhaloSensitivityResult`` wraps the generic ``PyAutoFit`` ``SensitivityResult`` with +convenience properties for the subhalo grid positions (``y``, ``x``), the detection +significance map, and Matplotlib visualisation helpers. +""" +import numpy as np +from typing import Optional, List, Tuple + +from autofit.non_linear.grid.sensitivity.result import SensitivityResult + +import autofit as af +import autoarray as aa + +from autolens.lens.tracer import Tracer + + +class SubhaloSensitivityResult(SensitivityResult): + def __init__( + self, + result: SensitivityResult, + ): + """ + The results of a subhalo sensitivity mapping analysis, where dark matter halos are used to simulate many + strong lens datasets which are fitted to quantify how detectable they are. + + Parameters + ---------- + result + The results of a sensitivity mapping analysis where. + """ + + super().__init__( + samples=result.samples, + perturb_samples=result.perturb_samples, + shape=result.shape, + path_values=result.path_values, + ) + + @property + def y(self) -> af.GridList: + """ + The y coordinates of the physical values of the sensitivity mapping grid. + + These are the `centre` coordinates of the dark matter subhalos that are included in the simulated datasets. + """ + return self.perturbed_physical_centres_list_from(path="mass.centre.centre_0") + + @property + def x(self) -> af.GridList: + """ + The x coordinates of the physical values of the sensitivity mapping grid. + + These are the `centre` coordinates of the dark matter subhalos that are included in the simulated datasets. + """ + return self.perturbed_physical_centres_list_from(path="mass.centre.centre_1") + + @property + def extent(self) -> Tuple[float, float, float, float]: + """ + The extent of the sensitivity mapping grid, which is the minimum and maximum values of the x and y coordinates. + """ + return (np.min(self.x), np.max(self.x), np.min(self.y), np.max(self.y)) + + def _array_2d_from(self, values) -> aa.Array2D: + """ + Returns an `Array2D` where the input values are reshaped from list of lists to a 2D array, which is + suitable for plotting. + + For example, this function may return the 2D array of the increases in log evidence for every lens model + fitted with a DM subhalo in the sensitivity mapping compared to the model without a DM subhalo. + + The orientation of the 2D array and its values are chosen to ensure that when this array is plotted, DM + subhalos with positive y and negative x `centre` coordinates appear in the top-left of the image. + + Parameters + ---------- + values_native + The list of list of values which are mapped to the 2D array (e.g. the `log_evidence` difference of every + lens model with a DM subhalo compared to the one without). + + Returns + ------- + The 2D array of values, where the values are mapped from the input list of lists. + """ + values_reshaped = [value for values in values.native for value in values] + + pixel_scale_list = [] + + for i in range(len(values_reshaped) - 1): + pixel_scale = abs(self.x[i] - self.x[i + 1]) + if pixel_scale > 0.0: + pixel_scale_list.append(pixel_scale) + + pixel_scales = np.min(pixel_scale_list) + + return aa.Array2D.from_yx_and_values( + y=self.y, + x=self.x, + values=values_reshaped, + pixel_scales=(pixel_scales, pixel_scales), + shape_native=self.shape, + ) + + def figure_of_merit_array( + self, + use_log_evidences: bool = True, + remove_zeros: bool = False, + ) -> aa.Array2D: + """ + Returns an `Array2D` where the values are the figure of merit (`log_evidence` or `log_likelihood` difference) + of every lens model on the sensitivity mapping grid. + + Values below zero may be rounded to zero, to prevent the figure of merit map being dominated by low values + + Parameters + ---------- + use_log_evidences + If `True`, the figure of merit values are the log evidences of every lens model on the grid search. + If `False`, they are the log likelihoods. + remove_zeros + If `True`, the figure of merit array is altered so that all values below 0.0 and set to 0.0. For plotting + relative figures of merit for Bayesian model comparison, this is convenient to remove negative values + and produce a clearer visualization of the overlay. + """ + + figures_of_merits = self.figure_of_merits( + use_log_evidences=use_log_evidences, + ) + + if remove_zeros: + figures_of_merits = af.GridList( + values=[fom if fom > 0.0 else 0.0 for fom in figures_of_merits], + shape=figures_of_merits.shape, + ) + + return self._array_2d_from(values=figures_of_merits) + diff --git a/autolens/lens/subhalo.py b/autolens/lens/subhalo.py index 759af930c..89835669a 100644 --- a/autolens/lens/subhalo.py +++ b/autolens/lens/subhalo.py @@ -1,176 +1,176 @@ -""" -Dark-matter subhalo detection via grid searches of non-linear searches. - -This module provides result containers and visualisation helpers for a subhalo detection -workflow in which a grid of ``PyAutoFit`` non-linear searches is run. Each cell of the -grid confines the subhalo's (y, x) centre to a small sub-region of the image plane using -uniform priors and fits the lens model with a subhalo included. - -``SubhaloGridSearchResult`` wraps ``af.GridSearchResult`` with: - -- ``y`` / ``x`` — the physical centre coordinates of each grid cell. -- ``log_evidence_differences`` — the Bayesian evidence improvement from adding a subhalo - relative to a smooth-model fit, useful for building a detection significance map. -- Plotting helpers that overlay the detection map on the lens image. -""" -import numpy as np -from typing import List, Optional, Tuple - -import autofit as af -import autoarray as aa - - -class SubhaloGridSearchResult(af.GridSearchResult): - def __init__( - self, - result: af.GridSearchResult, - ): - """ - The results of a subhalo detection analysis, where dark matter halos are added to the lens model and fitted - to the data. - - This result may use a grid search of non-linear searches where the (y,x) coordinates of each DM subhalo - included in the lens model are confined to a small region of the image plane via uniform priors. This object - contains functionality for creates ndarrays of these results for visualization and analysis. - - The samples of a previous lens model fit, not including a subhalo, may also be passed to this object. These - are used to plot all quantities relative to the no subhalo model, e.g. the change in log evidence. - - Parameters - ---------- - result - The results of a grid search of non-linear searches where each DM subhalo's (y,x) coordinates are - confined to a small region of the image plane via uniform priors. - """ - - super().__init__( - samples=result.samples, - lower_limits_lists=result.lower_limits_lists, - grid_priors=result.grid_priors, - ) - - @property - def y(self) -> List[float]: - """ - The y coordinates of the physical values of the subhalo grid, where each value is the centre of a grid cell. - - These are the `centre` coordinates of the dark matter subhalo priors. - """ - return self.physical_centres_lists_from( - path="galaxies.subhalo.mass.centre.centre_0" - ) - - @property - def x(self) -> List[float]: - """ - The x coordinates of the physical values of the subhalo grid, where each value is the centre of a grid cell. - - These are the `centre` coordinates of the dark matter subhalo priors. - """ - return self.physical_centres_lists_from( - path="galaxies.subhalo.mass.centre.centre_1" - ) - - @property - def extent(self) -> Tuple[float, float, float, float]: - """ - The extent of the sensitivity mapping grid, which is the minimum and maximum values of the x and y coordinates. - """ - return (np.min(self.x), np.max(self.x), np.min(self.y), np.max(self.y)) - - def _array_2d_from(self, values) -> aa.Array2D: - """ - Returns an `Array2D` where the input values are reshaped from list of lists to a 2D array, which is - suitable for plotting. - - For example, this function may return the 2D array of the increases in log evidence for every lens model - with a DM subhalo. - - The orientation of the 2D array and its values are chosen to ensure that when this array is plotted, DM - subhalos with positive y and negative x `centre` coordinates appear in the top-left of the image. - - Parameters - ---------- - values_native - The list of list of values which are mapped to the 2D array (e.g. the `log_evidence` increases of every - lens model with a DM subhalo). - - Returns - ------- - The 2D array of values, where the values are mapped from the input list of lists. - - """ - values_reshaped = [value for values in values.native for value in values] - - return aa.Array2D.from_yx_and_values( - y=self.y, - x=self.x, - values=values_reshaped, - pixel_scales=self.physical_step_sizes, - shape_native=self.shape, - ) - - def figure_of_merit_array( - self, - use_log_evidences: bool = True, - relative_to_value: float = 0.0, - remove_zeros: bool = False, - ) -> aa.Array2D: - """ - Returns an `Array2D` where the values are the figure of merit (`log_evidence` or `log_likelihood`) of every - lens model on the grid search. - - The values can be computed relative to an input value, `relative_to_value`, which is subtracted from the - figures of merit. This is typically the figure of merit of the no subhalo model, such that the values - represent the increase in the figure of merit when a subhalo is included in the lens model and thus - enable Bayesian model comparison to be performed. - - Values below zero may be rounded to zero, to prevent the figure of merit map being dominated by low values - - Parameters - ---------- - use_log_evidences - If `True`, the figure of merit values are the log evidences of every lens model on the grid search. - If `False`, they are the log likelihoods. - relative_to_value - The value to subtract from every figure of merit, which will typically be that of the lens model without - a so Bayesian model comparison can be easily performed. - remove_zeros - If `True`, the figure of merit array is altered so that all values below 0.0 and set to 0.0. For plotting - relative figures of merit for Bayesian model comparison, this is convenient to remove negative values - and produce a clearer visualization of the overlay. - """ - - figures_of_merits = self.figure_of_merits( - use_log_evidences=use_log_evidences, relative_to_value=relative_to_value - ) - - if remove_zeros: - figures_of_merits = af.GridList( - values=[fom if fom > 0.0 else 0.0 for fom in figures_of_merits], - shape=figures_of_merits.shape, - ) - - return self._array_2d_from(values=figures_of_merits) - - @property - def subhalo_mass_array(self) -> aa.Array2D: - """ - Returns an `Array2D` where the values are the `mass_at_200` of every DM subhalo of every lens model on the - grid search. - """ - return self._array_2d_from( - values=self.attribute_grid("galaxies.subhalo.mass.mass_at_200") - ) - - @property - def subhalo_centres_grid(self) -> aa.Grid2D: - """ - Returns a `Grid2D` where the values are the (y,x) coordinates of every DM subhalo of every lens model on - the grid search. - """ - return aa.Grid2D.no_mask( - values=np.asarray(self.attribute_grid("galaxies.subhalo.mass.centre")), - pixel_scales=self.physical_step_sizes, - shape_native=self.shape, - ) +""" +Dark-matter subhalo detection via grid searches of non-linear searches. + +This module provides result containers and visualisation helpers for a subhalo detection +workflow in which a grid of ``PyAutoFit`` non-linear searches is run. Each cell of the +grid confines the subhalo's (y, x) centre to a small sub-region of the image plane using +uniform priors and fits the lens model with a subhalo included. + +``SubhaloGridSearchResult`` wraps ``af.GridSearchResult`` with: + +- ``y`` / ``x`` — the physical centre coordinates of each grid cell. +- ``log_evidence_differences`` — the Bayesian evidence improvement from adding a subhalo + relative to a smooth-model fit, useful for building a detection significance map. +- Plotting helpers that overlay the detection map on the lens image. +""" +import numpy as np +from typing import List, Optional, Tuple + +import autofit as af +import autoarray as aa + + +class SubhaloGridSearchResult(af.GridSearchResult): + def __init__( + self, + result: af.GridSearchResult, + ): + """ + The results of a subhalo detection analysis, where dark matter halos are added to the lens model and fitted + to the data. + + This result may use a grid search of non-linear searches where the (y,x) coordinates of each DM subhalo + included in the lens model are confined to a small region of the image plane via uniform priors. This object + contains functionality for creates ndarrays of these results for visualization and analysis. + + The samples of a previous lens model fit, not including a subhalo, may also be passed to this object. These + are used to plot all quantities relative to the no subhalo model, e.g. the change in log evidence. + + Parameters + ---------- + result + The results of a grid search of non-linear searches where each DM subhalo's (y,x) coordinates are + confined to a small region of the image plane via uniform priors. + """ + + super().__init__( + samples=result.samples, + lower_limits_lists=result.lower_limits_lists, + grid_priors=result.grid_priors, + ) + + @property + def y(self) -> List[float]: + """ + The y coordinates of the physical values of the subhalo grid, where each value is the centre of a grid cell. + + These are the `centre` coordinates of the dark matter subhalo priors. + """ + return self.physical_centres_lists_from( + path="galaxies.subhalo.mass.centre.centre_0" + ) + + @property + def x(self) -> List[float]: + """ + The x coordinates of the physical values of the subhalo grid, where each value is the centre of a grid cell. + + These are the `centre` coordinates of the dark matter subhalo priors. + """ + return self.physical_centres_lists_from( + path="galaxies.subhalo.mass.centre.centre_1" + ) + + @property + def extent(self) -> Tuple[float, float, float, float]: + """ + The extent of the sensitivity mapping grid, which is the minimum and maximum values of the x and y coordinates. + """ + return (np.min(self.x), np.max(self.x), np.min(self.y), np.max(self.y)) + + def _array_2d_from(self, values) -> aa.Array2D: + """ + Returns an `Array2D` where the input values are reshaped from list of lists to a 2D array, which is + suitable for plotting. + + For example, this function may return the 2D array of the increases in log evidence for every lens model + with a DM subhalo. + + The orientation of the 2D array and its values are chosen to ensure that when this array is plotted, DM + subhalos with positive y and negative x `centre` coordinates appear in the top-left of the image. + + Parameters + ---------- + values_native + The list of list of values which are mapped to the 2D array (e.g. the `log_evidence` increases of every + lens model with a DM subhalo). + + Returns + ------- + The 2D array of values, where the values are mapped from the input list of lists. + + """ + values_reshaped = [value for values in values.native for value in values] + + return aa.Array2D.from_yx_and_values( + y=self.y, + x=self.x, + values=values_reshaped, + pixel_scales=self.physical_step_sizes, + shape_native=self.shape, + ) + + def figure_of_merit_array( + self, + use_log_evidences: bool = True, + relative_to_value: float = 0.0, + remove_zeros: bool = False, + ) -> aa.Array2D: + """ + Returns an `Array2D` where the values are the figure of merit (`log_evidence` or `log_likelihood`) of every + lens model on the grid search. + + The values can be computed relative to an input value, `relative_to_value`, which is subtracted from the + figures of merit. This is typically the figure of merit of the no subhalo model, such that the values + represent the increase in the figure of merit when a subhalo is included in the lens model and thus + enable Bayesian model comparison to be performed. + + Values below zero may be rounded to zero, to prevent the figure of merit map being dominated by low values + + Parameters + ---------- + use_log_evidences + If `True`, the figure of merit values are the log evidences of every lens model on the grid search. + If `False`, they are the log likelihoods. + relative_to_value + The value to subtract from every figure of merit, which will typically be that of the lens model without + a so Bayesian model comparison can be easily performed. + remove_zeros + If `True`, the figure of merit array is altered so that all values below 0.0 and set to 0.0. For plotting + relative figures of merit for Bayesian model comparison, this is convenient to remove negative values + and produce a clearer visualization of the overlay. + """ + + figures_of_merits = self.figure_of_merits( + use_log_evidences=use_log_evidences, relative_to_value=relative_to_value + ) + + if remove_zeros: + figures_of_merits = af.GridList( + values=[fom if fom > 0.0 else 0.0 for fom in figures_of_merits], + shape=figures_of_merits.shape, + ) + + return self._array_2d_from(values=figures_of_merits) + + @property + def subhalo_mass_array(self) -> aa.Array2D: + """ + Returns an `Array2D` where the values are the `mass_at_200` of every DM subhalo of every lens model on the + grid search. + """ + return self._array_2d_from( + values=self.attribute_grid("galaxies.subhalo.mass.mass_at_200") + ) + + @property + def subhalo_centres_grid(self) -> aa.Grid2D: + """ + Returns a `Grid2D` where the values are the (y,x) coordinates of every DM subhalo of every lens model on + the grid search. + """ + return aa.Grid2D.no_mask( + values=np.asarray(self.attribute_grid("galaxies.subhalo.mass.centre")), + pixel_scales=self.physical_step_sizes, + shape_native=self.shape, + ) diff --git a/autolens/lens/to_inversion.py b/autolens/lens/to_inversion.py index 9ca55c55c..ea9fc462b 100644 --- a/autolens/lens/to_inversion.py +++ b/autolens/lens/to_inversion.py @@ -1,508 +1,508 @@ -""" -Interface between a ``Tracer`` and the linear-algebra inversion module. - -``TracerToInversion`` extends the ``autogalaxy`` ``GalaxiesToInversion`` pattern to the -multi-plane lensing setting. It is responsible for: - -- Extracting every ``LightProfileLinear`` and ``Pixelization`` from the tracer's galaxies. -- Ray-tracing each linear profile's grid to the correct source plane using the tracer's - multi-plane deflection calculations. -- Assembling the ``mapping_matrix`` that maps source-plane parameters (intensities, mesh - coefficients) to image-plane pixels. -- Passing the assembled objects to ``autoarray``'s ``inversion_from`` factory so that the - linear system ``F x = d`` can be solved for the best-fit intensities / reconstructed - image. - -This class is not used directly by the user; it is instantiated inside -``FitImaging`` / ``FitInterferometer`` whenever the tracer contains linear components. -""" -from typing import Dict, List, Optional, Tuple, Type, Union - -import numpy as np - -from autonerves import cached_property - -import autoarray as aa -import autogalaxy as ag - -from autoarray.inversion.inversion.factory import inversion_from - - -class TracerToInversion(ag.AbstractToInversion): - def __init__( - self, - dataset: Optional[Union[aa.Imaging, aa.Interferometer, aa.DatasetInterface]], - tracer, - adapt_images: Optional[ag.AdaptImages] = None, - settings: aa.Settings = None, - xp=np, - preloads=None, - ): - """ - Interfaces a dataset and tracer with the inversion module, to setup a linear algebra calculation. - - The tracer's galaxies may contain linear light profiles whose `intensity` values are solved for via linear - algebra in order to best-fit the data. In this case, this class extracts the linear light profiles of all - galaxies, performs ray-tracing and computes their images and passes them to the `inversion` module such that - they become the `mapping_matrix` used in the linear algebra calculation. - - The galaxies may also contain pixelizations, which use a mesh (e.g. a Voronoi mesh) and regularization scheme - to reconstruct the galaxy's light. This class extracts all pixelizations, performs ray-tracing and uses the - pixelizations to set up `Mapper` objects which pair the dataset and pixelization to again set up the - appropriate `mapping_matrix` and other linear algebra matrices (e.g. the `regularization_matrix`). - - This class does not perform the inversion or compute any of the linear algebra matrices itself. Instead, - it acts as an interface between the dataset and galaxies and the inversion module, extracting the - necessary information from galaxies and passing it to the inversion module. - - The tracer's galaxies may also contain standard light profiles which have an input `intensity` which is not - solved for via linear algebra. These profiles should have already been evaluated and subtracted from the - dataset before the inversion is performed. This is how an inversion is set up in the fit - modules (e.g. `FitImaging`). - - Parameters - ---------- - dataset - The dataset containing the data which the inversion is performed on. - tracer - The tracer whose galaxies are fitted to the dataset via the inversion. - adapt_images - Images which certain pixelizations use to adapt their properties to the dataset, for example congregating - the pixelization's pixels to the brightest regions of the image. - settings - The settings of the inversion, which controls how the linear algebra calculation is performed. - """ - self.tracer = tracer - - self._preloads = preloads - - super().__init__( - dataset=dataset, - adapt_images=adapt_images, - settings=settings, - xp=xp, - ) - - @property - def planes(self) -> List[List[ag.Galaxy]]: - """ - The planes object of a tracer is a list of list of galaxies grouped into their planes, where planes - contained all galaxies at the same unique redshift. - - The planes are used to set up the inversion, whereby linear light profiles and pixelizations are extracted - and grouped based on the plane they are in. - - The reason for this is that ray-tracking is performed on a plane-by-plane basis, therefore using planes - makes it more straight forward to extract the appropriate traced grid for each galaxy. - - Returns - ------- - The planes of the tracer, which are used to set up the inversion. - """ - return self.tracer.planes - - @property - def has_mapper(self) -> bool: - """ - Checks whether the tracer has a pixelization, which is required to set up the inversion. - - This function is used to ensure computation run time is not wasted performing certain calculations if they are - not needed because the tracer does not have a pixelization. - - Returns - ------- - True if the tracer has a pixelization, False if not. - """ - for galaxies in self.planes: - if galaxies.has(cls=aa.Pixelization): - return True - - @cached_property - def traced_grid_2d_list_of_inversion(self) -> List[aa.type.Grid2DLike]: - """ - Returns a list of the traced grids of the inversion. - - For a standard two-plane lens system (e.g. a lens galaxy and source galaxy), assuming the lens galaxy - has linear light profiles and source galaxy has a pixelization, this function would return an image-plane - grid which has not been lensed and a source-plane grid which has been lensed. - - This function is short and could be called where it is used, however it is used in multiple functions - and therefore is cached to ensure it is not recalculated multiple times. - - Returns - ------- - The traced grids of the inversion, which are cached for efficiency. - """ - return self.tracer.traced_grid_2d_list_from( - grid=self.dataset.grids.pixelization, xp=self._xp - ) - - @cached_property - def lp_linear_func_list_galaxy_dict( - self, - ) -> Dict[ag.LightProfileLinearObjFuncList, ag.Galaxy]: - """ - Returns a dictionary associating each list of linear light profiles with the galaxy they belong to. - - You should first refer to the docstring of the `cls_light_profile_func_list_galaxy_dict_from` method in the - parent project PyAutoGalaxy for a description of this method. - - In brief, this method iterates over all galaxies and their light profiles, extracting their linear light - profiles and for each galaxy grouping them into a `LightProfileLinearObjFuncList` object, which is associated - with the galaxy via the dictionary. It also extracts linear light profiles from `Basis` objects and makes this - associated. - - When extracting the linear light profiles, ray-tracing is also performed to ensure that each grid input - into the linear light profile corresponds to the grid for the plane the galaxy is in, derived from the - galaxy's redshift. - - This function also handles some aspects of over-sampling, because the implementation of adaptive - over-sampling in a tracer is quite confusing. My hope is that this will be removed in the future, - so just try ignore it for now. - - The `LightProfileLinearObjFuncList` object contains the attributes (e.g. the data `grid` after ray tracing, - `light_profiles`) and functionality (e.g. a `mapping_matrix` method) that are required to perform the inversion. - - This function first creates a dictionary of linear light profiles associated with each galaxy for each plane, - and then does the same for all `Basis` objects. The two dictionaries are then combined and returned. - - Returns - ------- - A dictionary associating each list of linear light profiles and basis objects with the galaxy they belong to. - """ - if not self.tracer.perform_inversion: - return {} - - lp_linear_galaxy_dict_list = {} - - traced_grids_of_planes_list = self.tracer.traced_grid_2d_list_from( - grid=self.dataset.grids.lp, xp=self._xp - ) - - if self.dataset.grids.blurring is not None: - traced_blurring_grids_of_planes_list = self.tracer.traced_grid_2d_list_from( - grid=self.dataset.grids.blurring, xp=self._xp - ) - else: - traced_blurring_grids_of_planes_list = [None] * len( - traced_grids_of_planes_list - ) - - for plane_index, galaxies in enumerate(self.planes): - grids = aa.GridsInterface( - lp=traced_grids_of_planes_list[plane_index], - blurring=traced_blurring_grids_of_planes_list[plane_index], - ) - - dataset = aa.DatasetInterface( - data=self.dataset.data, - noise_map=self.dataset.noise_map, - grids=grids, - psf=self.psf, - transformer=self.transformer, - sparse_operator=self.dataset.sparse_operator, - ) - - galaxies_to_inversion = ag.GalaxiesToInversion( - dataset=dataset, - galaxies=galaxies, - settings=self.settings, - adapt_images=self.adapt_images, - xp=self._xp, - ) - - lp_linear_galaxy_dict_of_plane = ( - galaxies_to_inversion.lp_linear_func_list_galaxy_dict - ) - - lp_linear_galaxy_dict_list = { - **lp_linear_galaxy_dict_list, - **lp_linear_galaxy_dict_of_plane, - } - - return lp_linear_galaxy_dict_list - - def cls_pg_list_from(self, cls: Type) -> List[List]: - """ - Returns a list of lists of objects in the tracer which are an instance of the input `cls`, where each inner - list corresponds to a single plane, - - By grouping the objects extracted from this function (e.g. pixelizations, regularizations) by plane, it makes - it straight forward to pair them with the appropriate ray-traced grid. - - The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane - after being extracted from galaxies in the tracer. - - Parameters - ---------- - cls - The type of class that a list of instances of this class in the galaxy are returned for. - - Returns - ------- - The list of lists of objects that inherit from input `cls` in the galaxy grouped by plane. - """ - return [galaxies.cls_list_from(cls=cls) for galaxies in self.planes] - - @cached_property - def adapt_galaxy_image_pg_list(self) -> List[List[np.ndarray]]: - """ - Returns a list of lists of adapt images, where each inner list corresponds to a single plane. - - An adapt image is an image that certain pixelizations use to adapt their properties to the dataset, for example - congregating the pixelization's pixels to the brightest regions of the image. - - By grouping adapt images by plane, it makes it straight forward to pair them with the appropriate ray-traced - grid. - - The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane - after being extracted from galaxies in the tracer. - - Returns - ------- - The list of lists of adapt images grouped by plane. - """ - adapt_galaxy_image_pg_list = [] - - for galaxies in self.planes: - if galaxies.has(cls=aa.Pixelization): - plane_image_list = [] - - galaxies_with_pixelization_list = galaxies.galaxies_with_cls_list_from( - cls=aa.Pixelization - ) - - for galaxy in galaxies_with_pixelization_list: - if self.adapt_images is None: - image = None - else: - image = self.adapt_images.image_for_galaxy( - galaxy, self.tracer.galaxies - ) - - plane_image_list.append(image) - - adapt_galaxy_image_pg_list.append(plane_image_list) - - else: - adapt_galaxy_image_pg_list.append([]) - - return adapt_galaxy_image_pg_list - - @cached_property - def image_plane_mesh_grid_pg_list(self) -> List[List]: - """ - Returns a list of lists of image-plane mesh grids, where each inner list corresponds to a single plane. - - Certain pixelizations (e.g. the `VoronoiMagnification`) begin by placing what will become its the - source-pixel centres in the image-plane. This is done by calculating the centres in the image-plane - using an `image_mesh` object, and then ray-tracing these centres to the source-plane. - - This function computes the image-plane mesh grids for each plane, and returns them as a list of lists - grouped by plane. - - By grouping the image-plane mesh grids by plane, it makes it straight forward to pair them with the appropriate - ray-traced grid. - - The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane - after being extracted from galaxies in the tracer. - - Returns - ------- - The list of lists of image-plane mesh grids grouped by plane. - """ - if ( - self._preloads is not None - and self._preloads.image_plane_mesh_grid is not None - ): - return self._preloads.image_plane_mesh_grid - - image_plane_mesh_grid_list_of_planes = [] - - for galaxies in self.planes: - to_inversion = ag.GalaxiesToInversion( - dataset=self.dataset, - galaxies=galaxies, - adapt_images=self.adapt_images, - settings=self.settings, - xp=self._xp, - path_galaxies=self.tracer.galaxies, - ) - - image_plane_mesh_grid_list = to_inversion.image_plane_mesh_grid_list - image_plane_mesh_grid_list_of_planes.append(image_plane_mesh_grid_list) - - return image_plane_mesh_grid_list_of_planes - - @cached_property - def traced_mesh_grid_pg_list(self) -> List[List]: - """ - Returns a list of lists of traced mesh grids, where each inner list corresponds to a single plane. - - Certain pixelizations (e.g. the `VoronoiMagnification`) begin by placing what will become its the - source-pixel centres in the image-plane. This is done by calculating the centres in the image-plane - using an `image_mesh` object, and then ray-tracing these centres to the source-plane. - - This function then uses the tracer to ray-trace these image-plane mesh grids to the source-plane, returning - their grid of coordinates in the source-plane (e.g. after they have been lensed). These ray traced grids - are input into the inversion to ensure the source reconstruction occurs in the source-plane. - - By grouping the traced mesh grids by plane, it makes it straight forward to pair them with the appropriate - ray-traced grid. - - The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane - - Returns - ------- - The list of lists of traced mesh grids grouped by plane. - """ - if ( - self._preloads is not None - and self._preloads.source_plane_mesh_grid is not None - ): - # The shared-state path (e.g. `AnalysisImaging.shared_state_from`): the source-plane - # mesh geometry was traced once from the lead dataset and is reused here, so this - # dataset skips the image-mesh computation and mesh ray-trace. Its own (offset) data - # grid is still traced and mapped onto the shared mesh in `mapper_galaxy_dict`. - return self._preloads.source_plane_mesh_grid - - image_plane_mesh_grid_pg_list = self.image_plane_mesh_grid_pg_list - - traced_mesh_grid_pg_list = [] - - for plane_index, galaxies in enumerate(self.planes): - if image_plane_mesh_grid_pg_list[plane_index] is None: - traced_mesh_grid_pg_list.append(None) - else: - traced_mesh_grids_list = [] - - for image_plane_mesh_grid in image_plane_mesh_grid_pg_list[plane_index]: - try: - traced_mesh_grids_list.append( - self.tracer.traced_grid_2d_list_from( - grid=image_plane_mesh_grid, xp=self._xp - )[plane_index] - ) - except AttributeError: - traced_mesh_grids_list.append(None) - - traced_mesh_grid_pg_list.append(traced_mesh_grids_list) - - return traced_mesh_grid_pg_list - - @cached_property - def mapper_galaxy_dict(self) -> Dict[aa.Mapper, ag.Galaxy]: - """ - Returns a dictionary associating each `Mapper` object with the galaxy it belongs to. - - The docstring of the function `mapper_from` in PyAutoGalaxy describes the `Mapper` object in detail, and is used - in this function to create the `Mapper` objects which are associated with the galaxies. - - This function begins by extracting all galaxies with pixelizations, determining which have an image mesh - (see `image_plane_mesh_grid_pg_list`), which require an adapt image (see `adapt_galaxy_image_pg_list`), and - ray tracing these image-plane mesh grids to the source-plane (see `traced_mesh_grid_pg_list`). - - The tag `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane after being - extracted from galaxies in the tracer. Because all of these objects are grouped by plane, it makes it - straight forward for this function to pair them with the appropriate ray-traced grid and input them into - the mapper of that plane. - - This function essentially finds all galaxies with pixelizations, performs all necessary calculations to - set up the `Mapper` objects (e.g. compute the `image_plane_mesh_grid`), and then associates each `Mapper` - with the galaxy it belongs to. - - Returns - ------- - A dictionary associating each `Mapper` object with the galaxy it belongs to. - """ - if self._preloads is not None and self._preloads.mapper_galaxy_dict is not None: - return self._preloads.mapper_galaxy_dict - - if not self.has_mapper: - return {} - - mapper_galaxy_dict = {} - - traced_grids_of_planes_list = self.traced_grid_2d_list_of_inversion - - traced_mesh_grids_list_of_planes = self.traced_mesh_grid_pg_list - image_plane_mesh_grid_list = self.image_plane_mesh_grid_pg_list - - for plane_index, galaxies in enumerate(self.planes): - if galaxies.has(cls=aa.Pixelization): - to_inversion = ag.GalaxiesToInversion( - dataset=self.dataset, - galaxies=galaxies, - adapt_images=self.adapt_images, - settings=self.settings, - xp=self._xp, - ) - - galaxies_with_pixelization_list = galaxies.galaxies_with_cls_list_from( - cls=aa.Pixelization - ) - - for mapper_index in range( - len(traced_mesh_grids_list_of_planes[plane_index]) - ): - pixelization_list = self.cls_pg_list_from(cls=aa.Pixelization) - - try: - adapt_galaxy_image = self.adapt_galaxy_image_pg_list[ - plane_index - ][mapper_index] - except AttributeError: - adapt_galaxy_image = None - - mapper = to_inversion.mapper_from( - mesh=pixelization_list[plane_index][mapper_index].mesh, - regularization=pixelization_list[plane_index][ - mapper_index - ].regularization, - source_plane_data_grid=traced_grids_of_planes_list[plane_index], - source_plane_mesh_grid=traced_mesh_grids_list_of_planes[ - plane_index - ][mapper_index], - image_plane_mesh_grid=image_plane_mesh_grid_list[plane_index][ - mapper_index - ], - adapt_galaxy_image=adapt_galaxy_image, - ) - - galaxy = galaxies_with_pixelization_list[mapper_index] - - mapper_galaxy_dict[mapper] = galaxy - - return mapper_galaxy_dict - - @cached_property - def inversion(self): - """ - Returns an inversion object from the dataset, galaxies and inversion settings. - - The inversion uses all linear light profiles and pixelizations of the galaxies in the tracer to fit the data, - fully accounting for ray tracing. - - It solves for the linear light profile intensities and pixelization mesh pixel values via linear algebra, - finding the solution which best fits the data after regularization is applied. - - The `TracerToInversion` object acts as an interface between the dataset and tracer and the inversion module, - with many of its functions required to set up the inputs to the inversion object, primarily - the `linear_obj_list` and `linear_obj_galaxy_dict` properties. - - Returns - ------- - The inversion object which fits the dataset using the tracer. - """ - - inversion = inversion_from( - dataset=self.dataset, - linear_obj_list=self.linear_obj_list, - settings=self.settings, - xp=self._xp, - preloads=self._preloads, - ) - - inversion.linear_obj_galaxy_dict = self.linear_obj_galaxy_dict - - return inversion +""" +Interface between a ``Tracer`` and the linear-algebra inversion module. + +``TracerToInversion`` extends the ``autogalaxy`` ``GalaxiesToInversion`` pattern to the +multi-plane lensing setting. It is responsible for: + +- Extracting every ``LightProfileLinear`` and ``Pixelization`` from the tracer's galaxies. +- Ray-tracing each linear profile's grid to the correct source plane using the tracer's + multi-plane deflection calculations. +- Assembling the ``mapping_matrix`` that maps source-plane parameters (intensities, mesh + coefficients) to image-plane pixels. +- Passing the assembled objects to ``autoarray``'s ``inversion_from`` factory so that the + linear system ``F x = d`` can be solved for the best-fit intensities / reconstructed + image. + +This class is not used directly by the user; it is instantiated inside +``FitImaging`` / ``FitInterferometer`` whenever the tracer contains linear components. +""" +from typing import Dict, List, Optional, Tuple, Type, Union + +import numpy as np + +from autonerves import cached_property + +import autoarray as aa +import autogalaxy as ag + +from autoarray.inversion.inversion.factory import inversion_from + + +class TracerToInversion(ag.AbstractToInversion): + def __init__( + self, + dataset: Optional[Union[aa.Imaging, aa.Interferometer, aa.DatasetInterface]], + tracer, + adapt_images: Optional[ag.AdaptImages] = None, + settings: aa.Settings = None, + xp=np, + preloads=None, + ): + """ + Interfaces a dataset and tracer with the inversion module, to setup a linear algebra calculation. + + The tracer's galaxies may contain linear light profiles whose `intensity` values are solved for via linear + algebra in order to best-fit the data. In this case, this class extracts the linear light profiles of all + galaxies, performs ray-tracing and computes their images and passes them to the `inversion` module such that + they become the `mapping_matrix` used in the linear algebra calculation. + + The galaxies may also contain pixelizations, which use a mesh (e.g. a Voronoi mesh) and regularization scheme + to reconstruct the galaxy's light. This class extracts all pixelizations, performs ray-tracing and uses the + pixelizations to set up `Mapper` objects which pair the dataset and pixelization to again set up the + appropriate `mapping_matrix` and other linear algebra matrices (e.g. the `regularization_matrix`). + + This class does not perform the inversion or compute any of the linear algebra matrices itself. Instead, + it acts as an interface between the dataset and galaxies and the inversion module, extracting the + necessary information from galaxies and passing it to the inversion module. + + The tracer's galaxies may also contain standard light profiles which have an input `intensity` which is not + solved for via linear algebra. These profiles should have already been evaluated and subtracted from the + dataset before the inversion is performed. This is how an inversion is set up in the fit + modules (e.g. `FitImaging`). + + Parameters + ---------- + dataset + The dataset containing the data which the inversion is performed on. + tracer + The tracer whose galaxies are fitted to the dataset via the inversion. + adapt_images + Images which certain pixelizations use to adapt their properties to the dataset, for example congregating + the pixelization's pixels to the brightest regions of the image. + settings + The settings of the inversion, which controls how the linear algebra calculation is performed. + """ + self.tracer = tracer + + self._preloads = preloads + + super().__init__( + dataset=dataset, + adapt_images=adapt_images, + settings=settings, + xp=xp, + ) + + @property + def planes(self) -> List[List[ag.Galaxy]]: + """ + The planes object of a tracer is a list of list of galaxies grouped into their planes, where planes + contained all galaxies at the same unique redshift. + + The planes are used to set up the inversion, whereby linear light profiles and pixelizations are extracted + and grouped based on the plane they are in. + + The reason for this is that ray-tracking is performed on a plane-by-plane basis, therefore using planes + makes it more straight forward to extract the appropriate traced grid for each galaxy. + + Returns + ------- + The planes of the tracer, which are used to set up the inversion. + """ + return self.tracer.planes + + @property + def has_mapper(self) -> bool: + """ + Checks whether the tracer has a pixelization, which is required to set up the inversion. + + This function is used to ensure computation run time is not wasted performing certain calculations if they are + not needed because the tracer does not have a pixelization. + + Returns + ------- + True if the tracer has a pixelization, False if not. + """ + for galaxies in self.planes: + if galaxies.has(cls=aa.Pixelization): + return True + + @cached_property + def traced_grid_2d_list_of_inversion(self) -> List[aa.type.Grid2DLike]: + """ + Returns a list of the traced grids of the inversion. + + For a standard two-plane lens system (e.g. a lens galaxy and source galaxy), assuming the lens galaxy + has linear light profiles and source galaxy has a pixelization, this function would return an image-plane + grid which has not been lensed and a source-plane grid which has been lensed. + + This function is short and could be called where it is used, however it is used in multiple functions + and therefore is cached to ensure it is not recalculated multiple times. + + Returns + ------- + The traced grids of the inversion, which are cached for efficiency. + """ + return self.tracer.traced_grid_2d_list_from( + grid=self.dataset.grids.pixelization, xp=self._xp + ) + + @cached_property + def lp_linear_func_list_galaxy_dict( + self, + ) -> Dict[ag.LightProfileLinearObjFuncList, ag.Galaxy]: + """ + Returns a dictionary associating each list of linear light profiles with the galaxy they belong to. + + You should first refer to the docstring of the `cls_light_profile_func_list_galaxy_dict_from` method in the + parent project PyAutoGalaxy for a description of this method. + + In brief, this method iterates over all galaxies and their light profiles, extracting their linear light + profiles and for each galaxy grouping them into a `LightProfileLinearObjFuncList` object, which is associated + with the galaxy via the dictionary. It also extracts linear light profiles from `Basis` objects and makes this + associated. + + When extracting the linear light profiles, ray-tracing is also performed to ensure that each grid input + into the linear light profile corresponds to the grid for the plane the galaxy is in, derived from the + galaxy's redshift. + + This function also handles some aspects of over-sampling, because the implementation of adaptive + over-sampling in a tracer is quite confusing. My hope is that this will be removed in the future, + so just try ignore it for now. + + The `LightProfileLinearObjFuncList` object contains the attributes (e.g. the data `grid` after ray tracing, + `light_profiles`) and functionality (e.g. a `mapping_matrix` method) that are required to perform the inversion. + + This function first creates a dictionary of linear light profiles associated with each galaxy for each plane, + and then does the same for all `Basis` objects. The two dictionaries are then combined and returned. + + Returns + ------- + A dictionary associating each list of linear light profiles and basis objects with the galaxy they belong to. + """ + if not self.tracer.perform_inversion: + return {} + + lp_linear_galaxy_dict_list = {} + + traced_grids_of_planes_list = self.tracer.traced_grid_2d_list_from( + grid=self.dataset.grids.lp, xp=self._xp + ) + + if self.dataset.grids.blurring is not None: + traced_blurring_grids_of_planes_list = self.tracer.traced_grid_2d_list_from( + grid=self.dataset.grids.blurring, xp=self._xp + ) + else: + traced_blurring_grids_of_planes_list = [None] * len( + traced_grids_of_planes_list + ) + + for plane_index, galaxies in enumerate(self.planes): + grids = aa.GridsInterface( + lp=traced_grids_of_planes_list[plane_index], + blurring=traced_blurring_grids_of_planes_list[plane_index], + ) + + dataset = aa.DatasetInterface( + data=self.dataset.data, + noise_map=self.dataset.noise_map, + grids=grids, + psf=self.psf, + transformer=self.transformer, + sparse_operator=self.dataset.sparse_operator, + ) + + galaxies_to_inversion = ag.GalaxiesToInversion( + dataset=dataset, + galaxies=galaxies, + settings=self.settings, + adapt_images=self.adapt_images, + xp=self._xp, + ) + + lp_linear_galaxy_dict_of_plane = ( + galaxies_to_inversion.lp_linear_func_list_galaxy_dict + ) + + lp_linear_galaxy_dict_list = { + **lp_linear_galaxy_dict_list, + **lp_linear_galaxy_dict_of_plane, + } + + return lp_linear_galaxy_dict_list + + def cls_pg_list_from(self, cls: Type) -> List[List]: + """ + Returns a list of lists of objects in the tracer which are an instance of the input `cls`, where each inner + list corresponds to a single plane, + + By grouping the objects extracted from this function (e.g. pixelizations, regularizations) by plane, it makes + it straight forward to pair them with the appropriate ray-traced grid. + + The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane + after being extracted from galaxies in the tracer. + + Parameters + ---------- + cls + The type of class that a list of instances of this class in the galaxy are returned for. + + Returns + ------- + The list of lists of objects that inherit from input `cls` in the galaxy grouped by plane. + """ + return [galaxies.cls_list_from(cls=cls) for galaxies in self.planes] + + @cached_property + def adapt_galaxy_image_pg_list(self) -> List[List[np.ndarray]]: + """ + Returns a list of lists of adapt images, where each inner list corresponds to a single plane. + + An adapt image is an image that certain pixelizations use to adapt their properties to the dataset, for example + congregating the pixelization's pixels to the brightest regions of the image. + + By grouping adapt images by plane, it makes it straight forward to pair them with the appropriate ray-traced + grid. + + The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane + after being extracted from galaxies in the tracer. + + Returns + ------- + The list of lists of adapt images grouped by plane. + """ + adapt_galaxy_image_pg_list = [] + + for galaxies in self.planes: + if galaxies.has(cls=aa.Pixelization): + plane_image_list = [] + + galaxies_with_pixelization_list = galaxies.galaxies_with_cls_list_from( + cls=aa.Pixelization + ) + + for galaxy in galaxies_with_pixelization_list: + if self.adapt_images is None: + image = None + else: + image = self.adapt_images.image_for_galaxy( + galaxy, self.tracer.galaxies + ) + + plane_image_list.append(image) + + adapt_galaxy_image_pg_list.append(plane_image_list) + + else: + adapt_galaxy_image_pg_list.append([]) + + return adapt_galaxy_image_pg_list + + @cached_property + def image_plane_mesh_grid_pg_list(self) -> List[List]: + """ + Returns a list of lists of image-plane mesh grids, where each inner list corresponds to a single plane. + + Certain pixelizations (e.g. the `VoronoiMagnification`) begin by placing what will become its the + source-pixel centres in the image-plane. This is done by calculating the centres in the image-plane + using an `image_mesh` object, and then ray-tracing these centres to the source-plane. + + This function computes the image-plane mesh grids for each plane, and returns them as a list of lists + grouped by plane. + + By grouping the image-plane mesh grids by plane, it makes it straight forward to pair them with the appropriate + ray-traced grid. + + The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane + after being extracted from galaxies in the tracer. + + Returns + ------- + The list of lists of image-plane mesh grids grouped by plane. + """ + if ( + self._preloads is not None + and self._preloads.image_plane_mesh_grid is not None + ): + return self._preloads.image_plane_mesh_grid + + image_plane_mesh_grid_list_of_planes = [] + + for galaxies in self.planes: + to_inversion = ag.GalaxiesToInversion( + dataset=self.dataset, + galaxies=galaxies, + adapt_images=self.adapt_images, + settings=self.settings, + xp=self._xp, + path_galaxies=self.tracer.galaxies, + ) + + image_plane_mesh_grid_list = to_inversion.image_plane_mesh_grid_list + image_plane_mesh_grid_list_of_planes.append(image_plane_mesh_grid_list) + + return image_plane_mesh_grid_list_of_planes + + @cached_property + def traced_mesh_grid_pg_list(self) -> List[List]: + """ + Returns a list of lists of traced mesh grids, where each inner list corresponds to a single plane. + + Certain pixelizations (e.g. the `VoronoiMagnification`) begin by placing what will become its the + source-pixel centres in the image-plane. This is done by calculating the centres in the image-plane + using an `image_mesh` object, and then ray-tracing these centres to the source-plane. + + This function then uses the tracer to ray-trace these image-plane mesh grids to the source-plane, returning + their grid of coordinates in the source-plane (e.g. after they have been lensed). These ray traced grids + are input into the inversion to ensure the source reconstruction occurs in the source-plane. + + By grouping the traced mesh grids by plane, it makes it straight forward to pair them with the appropriate + ray-traced grid. + + The notation `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane + + Returns + ------- + The list of lists of traced mesh grids grouped by plane. + """ + if ( + self._preloads is not None + and self._preloads.source_plane_mesh_grid is not None + ): + # The shared-state path (e.g. `AnalysisImaging.shared_state_from`): the source-plane + # mesh geometry was traced once from the lead dataset and is reused here, so this + # dataset skips the image-mesh computation and mesh ray-trace. Its own (offset) data + # grid is still traced and mapped onto the shared mesh in `mapper_galaxy_dict`. + return self._preloads.source_plane_mesh_grid + + image_plane_mesh_grid_pg_list = self.image_plane_mesh_grid_pg_list + + traced_mesh_grid_pg_list = [] + + for plane_index, galaxies in enumerate(self.planes): + if image_plane_mesh_grid_pg_list[plane_index] is None: + traced_mesh_grid_pg_list.append(None) + else: + traced_mesh_grids_list = [] + + for image_plane_mesh_grid in image_plane_mesh_grid_pg_list[plane_index]: + try: + traced_mesh_grids_list.append( + self.tracer.traced_grid_2d_list_from( + grid=image_plane_mesh_grid, xp=self._xp + )[plane_index] + ) + except AttributeError: + traced_mesh_grids_list.append(None) + + traced_mesh_grid_pg_list.append(traced_mesh_grids_list) + + return traced_mesh_grid_pg_list + + @cached_property + def mapper_galaxy_dict(self) -> Dict[aa.Mapper, ag.Galaxy]: + """ + Returns a dictionary associating each `Mapper` object with the galaxy it belongs to. + + The docstring of the function `mapper_from` in PyAutoGalaxy describes the `Mapper` object in detail, and is used + in this function to create the `Mapper` objects which are associated with the galaxies. + + This function begins by extracting all galaxies with pixelizations, determining which have an image mesh + (see `image_plane_mesh_grid_pg_list`), which require an adapt image (see `adapt_galaxy_image_pg_list`), and + ray tracing these image-plane mesh grids to the source-plane (see `traced_mesh_grid_pg_list`). + + The tag `_pg_` stands for `plane galaxy`, and indicates that the objects are grouped by plane after being + extracted from galaxies in the tracer. Because all of these objects are grouped by plane, it makes it + straight forward for this function to pair them with the appropriate ray-traced grid and input them into + the mapper of that plane. + + This function essentially finds all galaxies with pixelizations, performs all necessary calculations to + set up the `Mapper` objects (e.g. compute the `image_plane_mesh_grid`), and then associates each `Mapper` + with the galaxy it belongs to. + + Returns + ------- + A dictionary associating each `Mapper` object with the galaxy it belongs to. + """ + if self._preloads is not None and self._preloads.mapper_galaxy_dict is not None: + return self._preloads.mapper_galaxy_dict + + if not self.has_mapper: + return {} + + mapper_galaxy_dict = {} + + traced_grids_of_planes_list = self.traced_grid_2d_list_of_inversion + + traced_mesh_grids_list_of_planes = self.traced_mesh_grid_pg_list + image_plane_mesh_grid_list = self.image_plane_mesh_grid_pg_list + + for plane_index, galaxies in enumerate(self.planes): + if galaxies.has(cls=aa.Pixelization): + to_inversion = ag.GalaxiesToInversion( + dataset=self.dataset, + galaxies=galaxies, + adapt_images=self.adapt_images, + settings=self.settings, + xp=self._xp, + ) + + galaxies_with_pixelization_list = galaxies.galaxies_with_cls_list_from( + cls=aa.Pixelization + ) + + for mapper_index in range( + len(traced_mesh_grids_list_of_planes[plane_index]) + ): + pixelization_list = self.cls_pg_list_from(cls=aa.Pixelization) + + try: + adapt_galaxy_image = self.adapt_galaxy_image_pg_list[ + plane_index + ][mapper_index] + except AttributeError: + adapt_galaxy_image = None + + mapper = to_inversion.mapper_from( + mesh=pixelization_list[plane_index][mapper_index].mesh, + regularization=pixelization_list[plane_index][ + mapper_index + ].regularization, + source_plane_data_grid=traced_grids_of_planes_list[plane_index], + source_plane_mesh_grid=traced_mesh_grids_list_of_planes[ + plane_index + ][mapper_index], + image_plane_mesh_grid=image_plane_mesh_grid_list[plane_index][ + mapper_index + ], + adapt_galaxy_image=adapt_galaxy_image, + ) + + galaxy = galaxies_with_pixelization_list[mapper_index] + + mapper_galaxy_dict[mapper] = galaxy + + return mapper_galaxy_dict + + @cached_property + def inversion(self): + """ + Returns an inversion object from the dataset, galaxies and inversion settings. + + The inversion uses all linear light profiles and pixelizations of the galaxies in the tracer to fit the data, + fully accounting for ray tracing. + + It solves for the linear light profile intensities and pixelization mesh pixel values via linear algebra, + finding the solution which best fits the data after regularization is applied. + + The `TracerToInversion` object acts as an interface between the dataset and tracer and the inversion module, + with many of its functions required to set up the inputs to the inversion object, primarily + the `linear_obj_list` and `linear_obj_galaxy_dict` properties. + + Returns + ------- + The inversion object which fits the dataset using the tracer. + """ + + inversion = inversion_from( + dataset=self.dataset, + linear_obj_list=self.linear_obj_list, + settings=self.settings, + xp=self._xp, + preloads=self._preloads, + ) + + inversion.linear_obj_galaxy_dict = self.linear_obj_galaxy_dict + + return inversion diff --git a/autolens/lens/tracer.py b/autolens/lens/tracer.py index 8eedb22b1..b4cf929d6 100644 --- a/autolens/lens/tracer.py +++ b/autolens/lens/tracer.py @@ -1,1246 +1,1246 @@ -""" -The core gravitational-lensing ray-tracing module for **PyAutoLens**. - -The central class is ``Tracer``, which groups a list of ``Galaxy`` objects by redshift -into a series of *planes* and performs multi-plane gravitational lensing calculations. -Key capabilities: - -- **Multi-plane ray tracing** — images are traced from the source plane through - intermediate lens planes to the observer using the cosmology-dependent angular - diameter distances between each pair of planes. -- **Lensed images** — the light of each galaxy is ray-traced to its correct position - and the contributions from all planes are summed. -- **Lensing maps** — convergence, shear, magnification, deflection angles, and potential - maps can all be computed on arbitrary ``Grid2D`` grids. -- **Critical curves & caustics** — the image-plane loci where magnification diverges and - their source-plane counterparts. -- **Lens modeling** — the ``Tracer`` is the core object used by - ``FitImaging`` / ``FitInterferometer`` / ``FitPointDataset`` when performing a - Bayesian model fit via a ``PyAutoFit`` non-linear search. -""" -from abc import ABC -import numpy as np -from scipy.interpolate import griddata -from typing import Dict, List, Optional, Type, Union - -import autofit as af -import autoarray as aa -import autogalaxy as ag - -from autogalaxy.profiles.geometry_profiles import GeometryProfile -from autogalaxy.profiles.light.snr import LightProfileSNR - -from autolens.lens import tracer_util - - -class Tracer(ABC, ag.OperateImageGalaxies): - def __init__( - self, - galaxies: Union[List[ag.Galaxy], af.ModelInstance], - cosmology: ag.cosmo.LensingCosmology = None, - ): - """ - Performs gravitational lensing ray-tracing calculations based on an input list of galaxies and a cosmology. - - The tracer stores the input galaxies in their input order, which may not be in ascending redshift order. - However, for all ray-tracing calculations, the tracer orders the input galaxies in ascending order of redshift, - as this is required for the multi-plane ray-tracing calculations. - - The tracer then creates a series of planes, where each plane is a collection of galaxies at the same redshift. - - The redshifts of these planes are determined by the redshifts of the galaxies, such that there is a unique - plane redshift for every unique galaxy redshift (galaxies with identical redshifts are put in the same plane). - - Gravitational lensing calculations are then performed individually for each plane and combined to produce the - correct overall lensing calculation. This includes the calculations like the deflection angles, create - images of the galaxies at different planes, and the overall lensed image of all galaxies. - - Multi-plane ray-tracing work natively, whereby the redshifts of the planes are used to perform multi-plane - ray-tracing calculations. This uses the input cosmology so that deflection-angles are rescaled according to - the lens-geometry of the multi-plane system. - - The `Tracer` object is also the core of the lens modeling API, whereby a model tracer is created via - the `PyAutoFit` `af.Model` object. - - Parameters - ---------- - galaxies - The list of galaxies which make up the gravitational lensing ray-tracing system. - cosmology - The cosmology used to perform ray-tracing calculations. - """ - - # if isinstance(galaxies, af.ModelInstance): - # galaxies = list(galaxies.values()) - - self.galaxies = galaxies - - self.cosmology = cosmology or ag.cosmo.Planck15() - - @property - def galaxies_ascending_redshift(self) -> List[ag.Galaxy]: - """ - Returns the galaxies in the tracer in ascending redshift order. - - Multi-plane ray tracing calculations begin from the first lowest redshift plane and perform calculations in - planes of increasing redshift. Thus, the galaxies are sorted by redshift in ascending order to aid this - calculation. - - When any galaxy has a JAX-traced ``redshift`` (e.g. a free-parameter - subhalo redshift under ``jax.jit``), Python's ``sorted`` cannot order - the list because the key comparator would coerce a traced boolean. - In that case, we trust the input order — the caller (typically - ``af.Collection(galaxies=...)``) is expected to declare galaxies in - ascending-redshift order, with each traced-redshift galaxy placed at - its intended plane position. See ``tracer_util.plane_redshifts_from`` - for the matching rule. - - Returns - ------- - The galaxies in the tracer in ascending redshift order. - """ - if not tracer_util._any_traced(self.galaxies): - return sorted(self.galaxies, key=lambda galaxy: galaxy.redshift) - - return list(self.galaxies) - - @property - def plane_redshifts(self) -> List[float]: - """ - Returns a list of plane redshifts from a list of galaxies, using the redshifts of the galaxies to determine the - unique redshifts of the planes. - - Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of - redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the - same plane. - - For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of - redshifts would be [1.0, 2.0]. - - Parameters - ---------- - galaxies - The list of galaxies used to determine the unique redshifts of the planes. - - Returns - ------- - The list of unique redshifts of the planes. - """ - return tracer_util.plane_redshifts_from( - galaxies=self.galaxies_ascending_redshift - ) - - @property - def planes(self) -> List[ag.Galaxies]: - """ - Returns a list of list of galaxies grouped into their planes, where planes contained all galaxies at the same - unique redshift. - - Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of - redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the - same plane. - - If the plane redshifts are not input, the redshifts of the galaxies are used to determine the unique redshifts of - the planes. - - For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of - list of galaxies would be [[g1, g2], g3]]. - - Parameters - ---------- - galaxies - The list of galaxies used to determine the unique redshifts of the planes. - plane_redshifts - The redshifts of the planes, which are used to group the galaxies into their respective planes. If not input, - the redshifts of the galaxies are used to determine the unique redshifts of the planes. - - Returns - ------- - The list of list of galaxies grouped into their planes. - """ - return tracer_util.planes_from( - galaxies=self.galaxies_ascending_redshift, - plane_redshifts=self.plane_redshifts, - ) - - @classmethod - def sliced_tracer_from( - cls, - lens_galaxies: List[ag.Galaxy], - line_of_sight_galaxies: List[ag.Galaxy], - source_galaxies: List[ag.Galaxy], - planes_between_lenses: List[int], - cosmology: ag.cosmo.LensingCosmology = None, - ): - """ - Returns a tracer where the lens system is split into planes with specified redshift distances between them. - - This is used for ray-tracing systems with many galaxies at different redshifts (e.g. hundreds or more). If - each galaxy redshift is treated indepedently, this would require many planes to be created, and the multi-plane - ray-tracing calculation would be computationally slow. - - To speed the calculation up, the galaxies are grouped into planes with redshifts separated by the inputs. - To achieve this, the galaxies have their redshifts reassigned from their original values to the nearest - value of a sliced plane redshift. This ensures that every galaxy is in a subset of planes. - - The redshifts of the planes are determines as follows: - - - Use the redshifts of the lens galaxies to determine the redshifts of the planes, where a lens galaxy is - expected to have a large mass and thus contribute to a significant portion of the overall lensing. This ensures - the main lens galaxies have a redshift and plane to themselves, ensuring calculation accuracy. - - - Use the redshift of the source galaxies to determine the redshift of the source plane, ensuring the source - galaxies also have a dedicated redshift and plane for calculation accuracy. - - - Create N planes between Earth and the first lens galaxy, the lens galaxy and the next lens galaxy (and so on) - up to the source galaxy. The number of planes between each set of galaxies is specified by the input - `planes_between_lenses`, where for a lens / source system `planes_between_lenses=[2,3]` would mean there are 2 - planes between Earth and the lens galaxy and 3 planes between the lens and source galaxy. - - - The `line_of_sight_galaxies` are placed in the planes corresponding to their closest redshift. - - Parameters - ---------- - lens_galaxies - The lens galaxies in the ray-tracing calculation. Most use cases will have only one lens galaxy, but the - API supports multiple lens galaxies (e.g. double Einstein ring systems). - line_of_sight_galaxies - The galaxies in the line-of-sight to the primary lens galaxy, which may have many different redshifts - and therefore create computational expensive multi-plane ray-tracing calculations without the plane - grouping provided by this method. - source_galaxies - The source galaxies in the ray-tracing calculation. The API only supports one source galaxy (input multiple - lens galaxies to build a multi-plane system). - planes_between_lenses - The number of slices between each main plane. The first entry in this list determines the number of slices - between Earth (redshift 0.0) and the first lens galaxy, the next between the lens and source, etc. - cosmology - The cosmology used to perform ray-tracing calculations. - """ - cosmology = cosmology or ag.cosmo.Planck15() - - lens_redshifts = tracer_util.plane_redshifts_from(galaxies=lens_galaxies) - - plane_redshifts = tracer_util.ordered_plane_redshifts_with_slicing_from( - lens_redshifts=lens_redshifts, - planes_between_lenses=planes_between_lenses, - source_plane_redshift=source_galaxies[0].redshift, - ) - - plane_redshifts.append(source_galaxies[0].redshift) - - galaxies = lens_galaxies + line_of_sight_galaxies + source_galaxies - - for galaxy in galaxies: - redshift_differences = list( - map(lambda z: abs(z - galaxy.redshift), plane_redshifts) - ) - galaxy.redshift = plane_redshifts[ - redshift_differences.index(min(redshift_differences)) - ] - - return Tracer(galaxies=galaxies, cosmology=cosmology) - - @property - def total_planes(self) -> int: - return len(self.plane_redshifts) - - @aa.decorators.to_grid - def traced_grid_2d_list_from( - self, grid: aa.type.Grid2DLike, xp=np, plane_index_limit: int = Optional[None] - ) -> List[aa.type.Grid2DLike]: - """ - Returns a ray-traced grid of 2D Cartesian (y,x) coordinates which accounts for multi-plane ray-tracing. - - This uses the redshifts and mass profiles of the galaxies contained within the tracer to perform the multi-plane - ray-tracing calculation. - - This function returns a list of 2D (y,x) grids, corresponding to each redshift in the input list of planes. The - plane redshifts are determined from the redshifts of the galaxies in each plane, whereby there is a unique plane - at each redshift containing all galaxies at the same redshift. - - For example, if the `planes` list contains three lists of galaxies with `redshift`'s z0.5, z=1.0 and z=2.0, the - returned list of traced grids will contain three entries corresponding to the input grid after ray-tracing to - redshifts 0.5, 1.0 and 2.0. - - An input cosmology object can change the cosmological model, which is used to compute the scaling - factors between planes (which are derived from their redshifts and angular diameter distances). It is these - scaling factors that account for multi-plane ray tracing effects. - - The calculation can be terminated early by inputting a `plane_index_limit`. All planes whose integer indexes are - above this value are omitted from the calculation and not included in the returned list of grids (the size of - this list is reduced accordingly). - - For example, if `planes` has 3 lists of galaxies, but `plane_index_limit=1`, the third plane (corresponding to - index 2) will not be calculated. The `plane_index_limit` is used to avoid uncessary ray tracing calculations - of higher redshift planes whose galaxies do not have mass profile (and only have light profiles). - - see `autolens.lens.tracer.tracer_util.traced_grid_2d_list_from()` for the full calculation. - - Parameters - ---------- - grid - The 2D (y, x) coordinates on which multi-plane ray-tracing calculations are performed. - plane_index_limit - The integer index of the last plane which is used to perform ray-tracing, all planes with an index above - this value are omitted. - - Returns - ------- - traced_grid_list - A list of 2D (y,x) grids each of which are the input grid ray-traced to a redshift of the input list of - planes. - """ - - grid_2d_list = tracer_util.traced_grid_2d_list_from( - planes=self.planes, - grid=grid, - cosmology=self.cosmology, - plane_index_limit=plane_index_limit, - xp=xp, - ) - - if isinstance(grid, aa.Grid2D): - grid_2d_over_sampled_list = tracer_util.traced_grid_2d_list_from( - planes=self.planes, - grid=grid.over_sampled, - cosmology=self.cosmology, - plane_index_limit=plane_index_limit, - xp=xp, - ) - - grid_2d_new_list = [] - - for i in range(len(grid_2d_list)): - grid_2d_new = aa.Grid2D( - values=grid_2d_list[i], - mask=grid.mask, - over_sampled=grid_2d_over_sampled_list[i], - over_sample_size=grid.over_sample_size, - over_sampler=grid.over_sampler, - ) - - grid_2d_new_list.append(grid_2d_new) - - return grid_2d_new_list - - return grid_2d_list - - def grid_2d_at_redshift_from( - self, grid: aa.type.Grid2DLike, redshift: float - ) -> aa.type.Grid2DLike: - """ - Returns a ray-traced grid of 2D Cartesian (y,x) coordinates, which accounts for multi-plane ray-tracing, at a - specified input redshift which may be different to the redshifts of all planes. - - Given a list of galaxies whose redshifts define a multi-plane lensing system and an input grid of (y,x) arc-second - coordinates (e.g. an image-plane grid), ray-trace the grid to an input redshift in of the multi-plane system. - - This is performed using multi-plane ray-tracing and a list of galaxies which are converted into a list of planes - at a set of redshift. The galaxy mass profiles are used to compute deflection angles. Any redshift can be input - even if a plane does not exist there, including redshifts before the first plane of the lens system. - - An input cosmology object can change the cosmological model, which is used to compute the scaling - factors between planes (which are derived from their redshifts and angular diameter distances). It is these - scaling factors that account for multi-plane ray tracing effects. - - There are two ways the calculation may be performed: - - 1) If the input redshift is the same as the redshift of a plane in the multi-plane system, the grid is ray-traced - to that plane and the traced grid returned. - - 2) If the input redshift is not the same as the redshift of a plane in the multi-plane system, a plane is inserted - at this redshift and the grid is ray-traced to this plane. - - For example, the input list `galaxies` may contained three `Galaxy` objects at redshifts z=0.5, z=1.0 and z=2.0. - We can input an image-plane grid and request that its coordinates are ray-traced to a plane at z=1.75 in this - multi-plane system. This will insert a plane at z=1.75 and use the galaxy's at z=0.5 and z=1.0 to compute - deflection angles, alongside accounting for multi-plane lensing effects via the angular diameter distances - between the different galaxy redshifts. - - Parameters - ---------- - redshift - The redshift the input (image-plane) grid is traced too. - galaxies - A list of galaxies which make up a multi-plane strong lens ray-tracing system. - grid - The 2D (y, x) coordinates which is ray-traced to the input redshift. - cosmology - The cosmology used for ray-tracing from which angular diameter distances between planes are computed. - """ - return tracer_util.grid_2d_at_redshift_from( - redshift=redshift, - galaxies=self.galaxies_ascending_redshift, - grid=grid, - cosmology=self.cosmology, - ) - - @property - def upper_plane_index_with_light_profile(self) -> int: - """ - Returns the index of the highest redshift plane in the tracer which has a light profile. - - When computing the image of a tracer, we only need to trace rays to the highest redshift plane which has a - light profile. This upper index is therefore used to do this, and ensure faster computation by avoiding - ray-tracing to planes which do not have light profiles. - - Returns - ------- - The index of the highest redshift plane in the tracer which has a light profile. - """ - return max( - [ - ( - plane_index - if any([galaxy.has(cls=ag.LightProfile) for galaxy in galaxies]) - else 0 - ) - for (plane_index, galaxies) in enumerate(self.planes) - ] - ) - - def image_2d_list_from( - self, - grid: aa.type.Grid2DLike, - xp=np, - operated_only: Optional[bool] = None, - ) -> List[aa.Array2D]: - """ - Returns a list of the 2D images for each plane from a 2D grid of Cartesian (y,x) coordinates. - - The image of each plane is computed by ray-tracing the grid using te mass profiles of each galaxies and then - summing the images of all galaxies in that plane. If a plane has no galaxies, or if the galaxies in a plane - has no light profiles, a numpy array of zeros is returned. - - For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies - at redshifts z=0.5 and z=1.0 have light and mass profiles, the returned list of images will be the image of the - galaxies at z=0.5 and z=1.0, where the image at redshift z=1.0 will include the lensing effects of the galaxies - at z=0.5. The image at redshift z=2.0 will be a numpy array of zeros. - - The `plane_index` input is used to return a specific image of a plane, as opposed to a list of images - of all planes. This can save on computational time when only the image of a specific plane is needed, - and is used to perform iterative over-sampling calculations. - - The images output by this function do not include instrument operations, such as PSF convolution (for imaging - data) or a Fourier transform (for interferometer data). - - Inherited methods in the `autogalaxy.operate.image` package can apply these operations to the images. - These functions may have the `operated_only` input passed to them, which is why this function includes - the `operated_only` input. - - If the `operated_only` input is included, the function omits light profiles which are parents of - the `LightProfileOperated` object, which signifies that the light profile represents emission that has - already had the instrument operations (e.g. PSF convolution, a Fourier transform) applied to it and therefore - that operation is not performed again. - - See the `autogalaxy.profiles.light` package for details of how images are computed from a light - profile. - - Parameters - ---------- - grid - The 2D (y, x) coordinates where values of the image are evaluated. - operated_only - The returned list from this function contains all light profile images, and they are never operated on - (e.g. via the imaging PSF). However, inherited methods in the `autogalaxy.operate.image` package can - apply these operations to the images, which may have the `operated_only` input passed to them. This input - therefore is used to pass the `operated_only` input to these methods. - """ - - traced_grid_list = self.traced_grid_2d_list_from( - grid=grid, - xp=xp, - plane_index_limit=self.upper_plane_index_with_light_profile, - ) - - image_2d_list = [] - - for plane_index in range(len(traced_grid_list)): - galaxies = self.planes[plane_index] - - image_2d = sum( - [ - galaxy.image_2d_from( - grid=traced_grid_list[plane_index], - operated_only=operated_only, - xp=xp, - ) - for galaxy in galaxies - ] - ) - - image_2d_list.append(image_2d) - - if self.upper_plane_index_with_light_profile < self.total_planes - 1: - if isinstance(grid, aa.Grid2D): - image_2d = aa.Array2D( - values=np.zeros(shape=grid.shape[0]), mask=grid.mask - ) - else: - image_2d = aa.ArrayIrregular(values=np.zeros(grid.shape[0])) - - for plane_index in range( - self.upper_plane_index_with_light_profile, self.total_planes - 1 - ): - image_2d_list.append(image_2d) - - return image_2d_list - - @aa.decorators.to_array - def image_2d_from( - self, - grid: aa.type.Grid2DLike, - xp=np, - operated_only: Optional[bool] = None, - ) -> aa.Array2D: - """ - Returns the 2D image of this ray-tracing strong lens system from a 2D grid of Cartesian (y,x) coordinates. - - This function first computes the image of each plane in the tracer, via the function `image_2d_list_from`. The - images are then summed to give the overall image of the tracer. - - Refer to the function `image_2d_list_from` for a full description of the calculation and how the `operated_only` - input is used. - - Parameters - ---------- - grid - The 2D (y, x) coordinates where values of the image are evaluated. - operated_only - The returned list from this function contains all light profile images, and they are never operated on - (e.g. via the imaging PSF). However, inherited methods in the `autogalaxy.operate.image` package can - apply these operations to the images, which may have the `operated_only` input passed to them. This input - therefore is used to pass the `operated_only` input to these methods. - """ - return sum( - self.image_2d_list_from(grid=grid, operated_only=operated_only, xp=xp) - ) - - def image_2d_via_input_plane_image_from( - self, - grid: aa.type.Grid2DLike, - plane_image: aa.Array2D, - xp=np, - plane_index: int = -1, - include_other_planes: bool = True, - ) -> aa.Array2D: - """ - Returns the lensed image of a plane or galaxy, where the input image is uniform and interpolated to compute - the lensed image. - - The typical use case is inputting the image of an irregular galaxy in the source-plane (whose values are - on a uniform array) and using this function computing the lensed image of this source galaxy. - - By default, this function computes the lensed image of the final plane, which is the source-plane, by using - `plane_index=-1`. For multi-plane lens systems, the lensed image of any planes can be computed by setting - `plane_index` to the index of the plane in the lens system. - - The emission of all other planes and galaxies can be included or omitted setting the `include_other_planes` - bool. If there are multiple planes in a multi-plane lens system, the emission of the other planes are fully - lensed. - - __Source Plane Interpolation__ - - We use the scipy interpolation function `griddata` to create the lensed source galaxy image. - - In brief, we trace light rays to the source plane and calculate values based on where those light rays land in - the source plane via interpolation. - - In more detail: - - - `points`: The 2D grid of (y,x) coordinates representing the location of every pixel of the source galaxy - image in the source-plane, from which we are creating the lensed source image. These coordinates are the - uniform source-plane grid computed after interpolating the irregular mesh the original source reconstruction - used. - - - `values`: The intensity values of the source galaxy image which is used to create the lensed source image. - These values are the flux values of the interpolated source galaxy image computed after interpolating the - irregular mesh the original source reconstruction used. - - - `xi`: The image-plane grid ray traced to the source-plane. This evaluates the flux of each image-plane - lensed source-pixel by ray-tracing it to the source-plane grid and computing its value by interpolating the - source galaxy image. - - Parameters - ---------- - grid - The image-plane grid which is traced to the plane where the image is computed, where these values are - used to perform the interpolation. - plane_image - The image of the plane or galaxy which is interpolated to compute the lensed image. - plane_index - The index of the plane the image is computed, where the default (-1) computes the image in the last plane - and therefore the source-plane. - - Returns - ------- - The lensed image of the plane or galaxy computed by interpolating its image to the image-plane. - """ - - plane_grid = aa.Grid2D.uniform( - shape_native=plane_image.shape_native, - pixel_scales=plane_image.pixel_scales, - ) - - traced_grid = self.traced_grid_2d_list_from( - grid=grid, plane_index_limit=plane_index, xp=xp - )[plane_index] - - image = griddata( - points=plane_grid.array, - values=plane_image.array, - xi=traced_grid.over_sampled.array, - fill_value=0.0, - method="linear", - ) - - if isinstance(grid, aa.Grid2D): - image = grid.over_sampler.binned_array_2d_from(array=image, xp=xp) - - if include_other_planes: - image_list = self.image_2d_list_from(grid=grid, xp=xp, operated_only=False) - - if plane_index < 0: - plane_index = self.total_planes + plane_index - - for plane_lp_index in range(self.total_planes): - if plane_lp_index != plane_index: - image += image_list[plane_lp_index] - - return aa.Array2D(values=image, mask=grid.mask, xp=xp) - - def galaxy_image_2d_dict_from( - self, grid: aa.type.Grid2DLike, xp=np, operated_only: Optional[bool] = None - ) -> Dict[ag.Galaxy, np.ndarray]: - """ - Returns a dictionary associating every `Galaxy` object in the `Tracer` with its corresponding 2D image, using - the instance of each galaxy as the dictionary keys. - - This object is used for adaptive-features, which use the image of each galaxy in a model-fit in order to - adapt quantities like a pixelization or regularization scheme to the surface brightness of the galaxies being - fitted. - - By inheriting from `OperateImageGalaxies` functions which apply operations of this dictionary are accessible, - for example convolving every image with a PSF or applying a Fourier transform to create a galaxy-visibilities - dictionary. - - Parameters - ---------- - grid - The 2D (y,x) coordinates of the (masked) grid, in its original geometric reference frame. - - Returns - ------- - A dictionary associated every galaxy in the tracer with its corresponding 2D image. - """ - - galaxy_image_2d_dict = dict() - - traced_grid_list = self.traced_grid_2d_list_from(grid=grid, xp=xp) - - for plane_index, galaxies in enumerate(self.planes): - image_2d_list = [ - galaxy.image_2d_from( - grid=traced_grid_list[plane_index], - operated_only=operated_only, - xp=xp, - ) - for galaxy in galaxies - ] - - for galaxy_index, galaxy in enumerate(galaxies): - galaxy_image_2d_dict[galaxy] = image_2d_list[galaxy_index] - - return galaxy_image_2d_dict - - @aa.decorators.to_vector_yx - def deflections_yx_2d_from( - self, grid: aa.type.Grid2DLike, xp=np - ) -> Union[aa.VectorYX2D, aa.VectorYX2DIrregular]: - """ - Returns the 2D deflection angles of all galaxies in the tracer, from the image-plane to the source-plane, - accounting for multi-plane ray tracing and from a 2D grid of Cartesian (y,x) coordinates. - - The multi-plane ray tracing calculations are performed in the function `traced_2d_grid_list_from` and its - sub-functions in the `tracer_util` module. This includes performing recursive ray-tracing between planes - based on the planes redshifts and using the cosmological distances between them to scale the deflection angles. - Users should refer to these functions for details on how the ray-tracing is performed. - - This function simply computes the corresponding multi-plane deflection angles by subtracting the image-plane - grid (e.g. before lensing) from the source-plane grid (e.g. after lensing). - - If there is only one plane in the tracer, the deflections are computed by summation of the deflections of all - galaxies in that plane. This is identical too, but computationally faster than, using the multi-plane - ray-tracing calculation. - - See the `autogalaxy.profiles.mass` package for details of how deflections are computed from a mass profile. - - Parameters - ---------- - grid - The 2D (y, x) coordinates where values of the deflections are evaluated. - """ - if self.total_planes > 1: - return self.deflections_between_planes_from(grid=grid, xp=xp) - return self.deflections_of_planes_summed_from(grid=grid, xp=xp) - - @aa.decorators.to_vector_yx - def deflections_of_planes_summed_from( - self, grid: aa.type.Grid2DLike, xp=np - ) -> Union[aa.VectorYX2D, aa.VectorYX2DIrregular]: - """ - Returns the summed 2D deflections angles of all galaxies in the tracer, not accounting for multi-plane ray - tracing, from a 2D grid of Cartesian (y,x) coordinates. - - The deflections of each plane is computed by summing the deflections of all galaxies in that plane. If a - plane has no galaxies, or if the galaxies in a plane has no mass profiles, a numpy array of zeros is returned. - - This calculation does not account for multi-plane ray-tracing effects, it is simply the sum of the deflections - of all galaxies. The function `deflections_between_planes_from` performs the calculation whilst - accounting for multi-plane ray-tracing effects. - - For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies - at redshifts z=0.5 and z=1.0 have mass profiles, the returned deflections will be the sum of the deflections - of the galaxies at z=0.5 and z=1.0. - - The deflections of a tracer do not depend on ray-tracing between grids. This is why the deflections of the - tracer is the sum of the deflections of all planes, and does not need to account for multi-plane ray-tracing - effects (in the way that deflection angles and images do). - - See the `autogalaxy.profiles.mass` package for details of how deflections are computed from a mass profile. - - Parameters - ---------- - grid - The 2D (y, x) coordinates where values of the deflections are evaluated. - """ - return sum( - [ - galaxy.deflections_yx_2d_from(grid=grid, xp=xp) - for galaxy in self.galaxies - ] - ) - - @aa.decorators.to_vector_yx - def deflections_between_planes_from( - self, grid: aa.type.Grid2DLike, xp=np, plane_i=0, plane_j=-1 - ) -> Union[aa.VectorYX2D, aa.VectorYX2DIrregular]: - """ - Returns the summed 2D deflections angles between two input planes in the tracer, accounting for multi-plane - ray tracing, from a 2D grid of Cartesian (y,x) coordinates. - - The multi-plane ray tracing calculations are performed in the function `traced_2d_grid_list_from` and its - sub-functions in the `tracer_util` module. This includes performing recursive ray-tracing between planes - based on the planes redshifts and using the cosmological distances between them to scale the deflection angles. - Users should refer to these functions for details on how the ray-tracing is performed. - - This function simply computes the corresponding multi-plane deflection angles by subtracting the grid - of index `plane_i` to that of index `plane_j`. The default inputs subtract the image-plane grid `plane_i=0` - (e.g. before lensing) from the source-plane grid `plane_j=-1` (e.g. after lensing). - - See the `autogalaxy.profiles.mass` package for details of how deflections are computed from a mass profile. - - Parameters - ---------- - grid - The 2D (y, x) coordinates where values of the deflections are evaluated. - """ - - traced_grids_list = self.traced_grid_2d_list_from(grid=grid, xp=xp) - - return traced_grids_list[plane_i] - traced_grids_list[plane_j] - - @aa.decorators.to_array - def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np) -> aa.Array2D: - """ - Returns the summed 2D convergence of all galaxies in the tracer from a 2D grid of Cartesian (y,x) coordinates. - - The convergence of each plane is computed by summing the convergences of all galaxies in that plane. If a - plane has no galaxies, or if the galaxies in a plane has no mass profiles, a numpy array of zeros is returned. - - For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies - at redshifts z=0.5 and z=1.0 have mass profiles, the returned convergence will be the sum of the convergences - of the galaxies at z=0.5 and z=1.0. - - The convergences of a tracer do not depend on ray-tracing between grids. This is why the convergence of the - tracer is the sum of the convergences of all planes, and does not need to account for multi-plane ray-tracing - effects (in the way that deflection angles and images do). - - See the `autogalaxy.profiles.mass` package for details of how convergences are computed from a mass profile. - - Parameters - ---------- - grid - The 2D (y, x) coordinates where values of the convergence are evaluated. - """ - return sum( - [galaxy.convergence_2d_from(grid=grid, xp=xp) for galaxy in self.galaxies] - ) - - @aa.decorators.to_array - def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np) -> aa.Array2D: - """ - Returns the summed 2D potential of all galaxies in the tracer from a 2D grid of Cartesian (y,x) coordinates. - - The potential of each plane is computed by summing the potentials of all galaxies in that plane. If a - plane has no galaxies, or if the galaxies in a plane has no mass profiles, a numpy array of zeros is returned. - - For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies - at redshifts z=0.5 and z=1.0 have mass profiles, the returned potential will be the sum of the potentials - of the galaxies at z=0.5 and z=1.0. - - The potentials of a tracer do not depend on ray-tracing between grids. This is why the potential of the - tracer is the sum of the potentials of all planes, and does not need to account for multi-plane ray-tracing - effects (in the way that deflection angles and images do). - - See the `autogalaxy.profiles.mass` package for details of how potentials are computed from a mass profile. - - Parameters - ---------- - grid - The 2D (y, x) coordinates where values of the potential are evaluated. - """ - return sum( - [galaxy.potential_2d_from(grid=grid, xp=xp) for galaxy in self.galaxies] - ) - - @aa.decorators.to_array - def time_delays_from(self, grid: aa.type.Grid2DLike, xp=np) -> aa.Array2D: - """ - Returns the gravitational lensing time delay in days, for a grid of 2D (y, x) coordinates. - - This function calculates the time delay at each image-plane position due to both geometric and gravitational - (Shapiro) effects, as described by the Fermat potential, which are computed using the deflection angles of the - galaxies in the lens system. - - Time dleays are computed from a reference point, which is the delay one would compute without a mass model. - This means a time delay could be negative, which means the light travel time is faster when lensing is - accounted for. When performing fitting, all time-delays are subtracted by the time-delay of the - shortest time-delay, such that its value is zero and all other time-delays are positive. - - A full description of the calculation is given in the `autolens.lens.tracer.tracer_util.time_delays_from` - function, which performs the calculation and has full latex documentation of the equations used. - """ - return tracer_util.time_delays_from( - galaxies=ag.Galaxies(self.galaxies_ascending_redshift), - grid=grid, - xp=xp, - cosmology=self.cosmology, - ) - - def has(self, cls: Type) -> bool: - """ - Returns a bool specifying whether this tracer has a galaxy with a certain class type. - - For example, for the input `cls=ag.LightProfile`, this function returns True if any galaxy in the tracer has a - light profile and false if no galaxy has a light profile. - - This function is used to check for mass profiles and specific types of profiles, like the linear light profile. - - Parameters - ---------- - cls - The class type of the galaxy which is checked for in the tracer. - - Returns - ------- - True if any galaxy in the tracer has the input class type, else False. - """ - return any(map(lambda galaxy: galaxy.has(cls=cls), self.galaxies)) - - def cls_list_from(self, cls: Type) -> List: - """ - Returns a list of objects in the tracer which are an instance of the input `cls`. - - For example: - - - If the input is `cls=ag.LightProfile`, a list containing all light profiles in the tracer is returned. - - Returns - ------- - The list of objects in the tracer that inherit from input `cls`. - """ - cls_list = [] - - for galaxy in self.galaxies: - if galaxy.has(cls=cls): - for cls_galaxy in galaxy.cls_list_from(cls=cls): - cls_list.append(cls_galaxy) - - return cls_list - - def plane_index_via_redshift_from(self, redshift: float) -> Optional[int]: - """ - Returns the index of a plane at a given redshift. - - This is used to determine the index of a plane in the tracer, which is useful for multi-plane ray-tracing - calculations. The index of the plane may, for example, be used to extract grid of a specific plane in the - tracer after multi-plane ray-tracing calculations. - - A tolerance of 1e-8 is used to determine if the input redshift is close to the redshift of a plane. If - no matching plane is found, None is returned. - - Parameters - ---------- - redshift - The redshift of the plane to find the index of. - - Returns - ------- - The index of the plane that matches the input redshift. - """ - - for plane_index, plane_redshift in enumerate(self.plane_redshifts): - if np.isclose(redshift, plane_redshift, atol=1e-8): - return plane_index - - @property - def plane_indexes_with_pixelizations(self) -> List[int]: - """ - Returns a list of integer indexes of the indexes of planes which use a `Pixelization` to reconstruct the - source galaxy. - - This list is used to set up an inversion, whereby each pixelization is extracted from the tracer with its - corresponding ray-traced grid and passed to the PyAutoArray `inversion` module. - - Returns - ------- - The list of integer indexes of the planes which use a `Pixelization` to reconstruct the source galaxy. - """ - plane_indexes_with_inversions = [ - plane_index if plane.has(cls=aa.Pixelization) else None - for (plane_index, plane) in enumerate(self.planes) - ] - return [ - plane_index - for plane_index in plane_indexes_with_inversions - if plane_index is not None - ] - - @property - def plane_indexes_with_images(self): - """ - Returns a list of integer indexes of the indexes of planes which create an image, meaning they either - have a `LightProfile` or `Pixelization`. - - This list is used to visualize double source plane lenses, whereby a fit for every plane with a - `LightProfile` or `Pixelization` is created. - - Returns - ------- - The list of integer indexes of the planes which create an image. - """ - plane_indexes_with_images = [ - plane_index if plane.has(cls=ag.LightProfile) else None - for (plane_index, plane) in enumerate(self.planes) - ] + self.plane_indexes_with_pixelizations - - plane_indexes_with_images = list(dict.fromkeys(plane_indexes_with_images)) - - return [ - plane_index - for plane_index in plane_indexes_with_images - if plane_index is not None - ] - - @property - def perform_inversion(self) -> bool: - """ - Returns a bool specifying whether this fit object performs an inversion. - - This is based on whether any of the galaxies have a `Pixelization` or `LightProfileLinear` object, in which - case an inversion is performed. - - Returns - ------- - A bool which is True if an inversion is performed. - """ - return any(plane.perform_inversion for plane in self.planes) - - def extract_attribute( - self, cls: Type, attr_name: str, filter_nones: Optional[bool] = False - ) -> List[Union[aa.ArrayIrregular, aa.Grid2DIrregular]]: - """ - Returns an extracted attribute of a class in the tracer as a `ValueIrregular` or `Grid2DIrregular` object. - - For example, if a tracer has a galaxy with two light profiles, the input: - - `tracer.extract_attribute(cls=LightProfile, name="axis_ratio")` - - Returns - - `ArrayIrregular(values=[axis_ratio_0, axis_ratio_1])` - - If the image plane has two galaxies with two mass profiles and the source plane another galaxy with a - mass profile, the input: - - `tracer.extract_attribute(cls=MassProfile, name="centre")` - - Returns - - GridIrregular2D(grid=[(centre_y_0, centre_x_0), (centre_y_1, centre_x_1), (centre_y_2, centre_x_2)]) - - The primary use of this function is to extract the attributes of profiles for visualization, for example - plotting the centres of all mass profiles colored by their profile over the tracer's image. - - Parameters - ---------- - cls - The class type of object whose attribute is extracted (e.g. light profile, mass profile). - attr_name - The name of the attribute which is extracted from the class type (e.g. axis_ratio, centre). - """ - - def extract(value, name): - try: - return getattr(value, name) - except (AttributeError, IndexError): - return None - - attributes = [ - extract(value, attr_name) - for galaxy in self.galaxies - for value in galaxy.__dict__.values() - if isinstance(value, cls) - ] - - if attributes == []: - return None - elif isinstance(attributes[0], float): - return aa.ArrayIrregular(values=attributes) - elif isinstance(attributes[0], tuple): - return aa.Grid2DIrregular(values=attributes) - - def extract_attributes_of_planes( - self, cls: Type, attr_name: str, filter_nones: Optional[bool] = False - ) -> List[Union[aa.ArrayIrregular, aa.Grid2DIrregular]]: - """ - Returns an extracted attribute of a class in the tracer as a list of `ValueIrregular` or `Grid2DIrregular` - objects, where the indexes of the list correspond to the tracer's planes. - - For example, if a tracer has an image-plane with a galaxy with a light profile and a source-plane with a galaxy - with a light profile, the input: - - `tracer.extract_attributes_of_planes(cls=LightProfile, name="axis_ratio")` - - Returns: - - [ArrayIrregular(values=[axis_ratio_0]), ArrayIrregular(values=[axis_ratio_1])] - - If the image plane has two galaxies with a mass profile each and the source plane another galaxy with a - mass profile, input: - - `tracer.extract_attributes_of_planes(cls=MassProfile, name="centres")` - - Returns: - - [ - Grid2DIrregular(values=[(centre_y_0, centre_x_0)]), - Grid2DIrregular(values=[(centre_y_0, centre_x_0), (centre_y_1, centre_x_1)]) - ] - - If a profile does not have a certain entry, it is replaced with a None. The Nones can be removed - by setting `filter_nones=True`. - - The primary use of this function is to extract the attributes of profiles for visualization, for example - plotting the centres of all mass profiles colored by their profile over the tracer's image. - - Parameters - ---------- - cls - The class type of object whose attribute is extracted (e.g. light profile, mass profile). - attr_name - The name of the attribute which is extracted from the class type (e.g. axis_ratio, centre). - filter_nones - If True, None entries are removed from the list. - """ - - attribute_list = [] - - for plane in self.planes: - for galaxy in plane: - attribute_list += [ - galaxy.extract_attribute(cls=cls, attr_name=attr_name) - ] - - if filter_nones: - return list(filter(None, attribute_list)) - - return attribute_list - - def extract_attributes_of_galaxies( - self, cls: Type, attr_name: str, filter_nones: Optional[bool] = False - ) -> List[Union[aa.ArrayIrregular, aa.Grid2DIrregular]]: - """ - Returns an attribute of a class in the tracer as a list of `ValueIrregular` or `Grid2DIrregular` objects, where - the indexes of the list correspond to the tracer's galaxies. If a plane has multiple galaxies it will have a - list with each galaxy as an entry. - - For example, if a tracer has an image-plane with a galaxy with a light profile and a source-plane with a galaxy - with a light profile, the input: - - `tracer.extract_attributes_of_galaxies(cls=LightProfile, name="axis_ratio")` - - Returns: - - [ArrayIrregular(values=[axis_ratio_0]), ArrayIrregular(values=[axis_ratio_1])] - - If the image plane has two galaxies with a mass profile each and the source plane another galaxy with a - mass profile, the input: - - `tracer.extract_attributes_of_galaxies(cls=MassProfile, name="centres")` - - Returns: - - [ - Grid2DIrregular(values=[(centre_y_0, centre_x_0)]), - Grid2DIrregular(values=[(centre_y_0, centre_x_0)]) - Grid2DIrregular(values=[(centre_y_0, centre_x_0)]) - ] - - If the first galaxy in the image plane in the example above had two mass profiles as well as the galaxy in the - source plane it would return: - - [ - Grid2DIrregular(values=[(centre_y_0, centre_x_0), (centre_y_1, centre_x_1)]), - Grid2DIrregular(values=[(centre_y_0, centre_x_0)]) - Grid2DIrregular(values=[(centre_y_0, centre_x_0, (centre_y_1, centre_x_1))]) - ] - - If a profile does not have a certain entry, it is replaced with a None. The Nones can be removed - by setting `filter_nones=True`. - - The primary use of this function is to extract the attributes of profiles for visualization, for example - plotting the centres of all mass profiles colored by their profile over the tracer's image. - - Parameters - ---------- - cls - The class type of object whose attribute is extracted (e.g. light profile, mass profile). - attr_name - The name of the attribute which is extracted from the class type (e.g. axis_ratio, centre). - filter_nones - """ - if filter_nones: - return [ - galaxy.extract_attribute(cls=cls, attr_name=attr_name) - for galaxy in self.galaxies - if galaxy.extract_attribute(cls=cls, attr_name=attr_name) is not None - ] - - else: - return [ - galaxy.extract_attribute(cls=cls, attr_name=attr_name) - for galaxy in self.galaxies - ] - - def extract_profile(self, profile_name: str) -> GeometryProfile: - """ - Returns a profile (e.g. a `LightProfile`, `MassProfile`, `Point`) from the tracer using the name of that - component. - - For example, if a tracer has two galaxies named `lens` and `source`, where `lens` has a light profile - named `light_0` and `source` has a light profile named `light_1`, the input: - - `tracer.extract_profile(profile_name="light_1")` - - Return the light profile of the source galaxy. - - This primarily used for point-source modeling, where the locations that the point-sources tracer to in - different planes must be paired to their corresponding point-source `Point` profile. - - Parameters - ---------- - profile_name - The name of the profile component in the tracer. - """ - for galaxy in self.galaxies: - try: - return galaxy.__dict__[profile_name] - except KeyError: - pass - - def extract_plane_index_of_profile(self, profile_name: str) -> int: - """ - Returns the plane index of a profile (e.g. a `LightProfile`, `MassProfile`, `Point`) from the tracer using - the name of that component. - - For example, if a tracer has two galaxies named `lens` and `source`, where `lens` has a light profile - named `light_0` and `source` has a light profile named `light_1`, the input: - - `tracer.extract_profile(profile_name="light_1")` - - Would return `plane_index=1` corresponding to the profile in the source plane. - - This primarily used for point-source modeling, where the locations that the point-sources tracer to in - different planes must be paired to their corresponding point-source `Point` profile. - - Parameters - ---------- - profile_name - The name of the profile component in the tracer. - """ - for plane_index, galaxies in enumerate(self.planes): - for galaxy in galaxies: - if profile_name in galaxy.__dict__: - return plane_index - - def set_snr_of_snr_light_profiles( - self, - grid: aa.type.Grid2DLike, - exposure_time: float, - background_sky_level: float = 0.0, - psf: Optional[aa.Convolver] = None, - ): - """ - Iterate over every `LightProfileSNR` in the tracer and set their `intensity` values to values which give - their input `signal_to_noise_ratio` value, which is performed as follows: - - - Evaluate the image of each light profile on the input grid. - - Blur this image with a PSF, if included. - - Take the value of the brightest pixel. - - Use an input `exposure_time` and `background_sky` (e.g. from the `SimulatorImaging` object) to determine - what value of `intensity` gives the desired signal to noise ratio for the image. - - The intensity is set using an input grid, meaning that for strong lensing calculations the ray-traced grid - can be used such that the S/N accounts for the magnification of a source galaxy. - - Parameters - ---------- - grid - The (y, x) coordinates in the original reference frame of the grid. - exposure_time - The exposure time of the simulated imaging. - background_sky_level - The level of the background sky of the simulated imaging. - psf - The psf of the simulated imaging which can change the S/N of the light profile due to spreading out - the emission. - """ - has_snr_profile = any( - isinstance(light_profile, LightProfileSNR) - for galaxies in self.planes - for galaxy in galaxies - for light_profile in galaxy.cls_list_from(cls=ag.LightProfile) - ) - if not has_snr_profile: - return - - grid = aa.Grid2D.uniform( - shape_native=grid.shape_native, - pixel_scales=grid.pixel_scales, - ) - - traced_grids_of_planes_list = self.traced_grid_2d_list_from(grid=grid) - - for plane_index, galaxies in enumerate(self.planes): - for galaxy in galaxies: - for light_profile in galaxy.cls_list_from(cls=ag.LightProfile): - if isinstance(light_profile, LightProfileSNR): - light_profile.set_intensity_from( - grid=traced_grids_of_planes_list[plane_index], - exposure_time=exposure_time, - background_sky_level=background_sky_level, - psf=psf, - ) +""" +The core gravitational-lensing ray-tracing module for **PyAutoLens**. + +The central class is ``Tracer``, which groups a list of ``Galaxy`` objects by redshift +into a series of *planes* and performs multi-plane gravitational lensing calculations. +Key capabilities: + +- **Multi-plane ray tracing** — images are traced from the source plane through + intermediate lens planes to the observer using the cosmology-dependent angular + diameter distances between each pair of planes. +- **Lensed images** — the light of each galaxy is ray-traced to its correct position + and the contributions from all planes are summed. +- **Lensing maps** — convergence, shear, magnification, deflection angles, and potential + maps can all be computed on arbitrary ``Grid2D`` grids. +- **Critical curves & caustics** — the image-plane loci where magnification diverges and + their source-plane counterparts. +- **Lens modeling** — the ``Tracer`` is the core object used by + ``FitImaging`` / ``FitInterferometer`` / ``FitPointDataset`` when performing a + Bayesian model fit via a ``PyAutoFit`` non-linear search. +""" +from abc import ABC +import numpy as np +from scipy.interpolate import griddata +from typing import Dict, List, Optional, Type, Union + +import autofit as af +import autoarray as aa +import autogalaxy as ag + +from autogalaxy.profiles.geometry_profiles import GeometryProfile +from autogalaxy.profiles.light.snr import LightProfileSNR + +from autolens.lens import tracer_util + + +class Tracer(ABC, ag.OperateImageGalaxies): + def __init__( + self, + galaxies: Union[List[ag.Galaxy], af.ModelInstance], + cosmology: ag.cosmo.LensingCosmology = None, + ): + """ + Performs gravitational lensing ray-tracing calculations based on an input list of galaxies and a cosmology. + + The tracer stores the input galaxies in their input order, which may not be in ascending redshift order. + However, for all ray-tracing calculations, the tracer orders the input galaxies in ascending order of redshift, + as this is required for the multi-plane ray-tracing calculations. + + The tracer then creates a series of planes, where each plane is a collection of galaxies at the same redshift. + + The redshifts of these planes are determined by the redshifts of the galaxies, such that there is a unique + plane redshift for every unique galaxy redshift (galaxies with identical redshifts are put in the same plane). + + Gravitational lensing calculations are then performed individually for each plane and combined to produce the + correct overall lensing calculation. This includes the calculations like the deflection angles, create + images of the galaxies at different planes, and the overall lensed image of all galaxies. + + Multi-plane ray-tracing work natively, whereby the redshifts of the planes are used to perform multi-plane + ray-tracing calculations. This uses the input cosmology so that deflection-angles are rescaled according to + the lens-geometry of the multi-plane system. + + The `Tracer` object is also the core of the lens modeling API, whereby a model tracer is created via + the `PyAutoFit` `af.Model` object. + + Parameters + ---------- + galaxies + The list of galaxies which make up the gravitational lensing ray-tracing system. + cosmology + The cosmology used to perform ray-tracing calculations. + """ + + # if isinstance(galaxies, af.ModelInstance): + # galaxies = list(galaxies.values()) + + self.galaxies = galaxies + + self.cosmology = cosmology or ag.cosmo.Planck15() + + @property + def galaxies_ascending_redshift(self) -> List[ag.Galaxy]: + """ + Returns the galaxies in the tracer in ascending redshift order. + + Multi-plane ray tracing calculations begin from the first lowest redshift plane and perform calculations in + planes of increasing redshift. Thus, the galaxies are sorted by redshift in ascending order to aid this + calculation. + + When any galaxy has a JAX-traced ``redshift`` (e.g. a free-parameter + subhalo redshift under ``jax.jit``), Python's ``sorted`` cannot order + the list because the key comparator would coerce a traced boolean. + In that case, we trust the input order — the caller (typically + ``af.Collection(galaxies=...)``) is expected to declare galaxies in + ascending-redshift order, with each traced-redshift galaxy placed at + its intended plane position. See ``tracer_util.plane_redshifts_from`` + for the matching rule. + + Returns + ------- + The galaxies in the tracer in ascending redshift order. + """ + if not tracer_util._any_traced(self.galaxies): + return sorted(self.galaxies, key=lambda galaxy: galaxy.redshift) + + return list(self.galaxies) + + @property + def plane_redshifts(self) -> List[float]: + """ + Returns a list of plane redshifts from a list of galaxies, using the redshifts of the galaxies to determine the + unique redshifts of the planes. + + Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of + redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the + same plane. + + For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of + redshifts would be [1.0, 2.0]. + + Parameters + ---------- + galaxies + The list of galaxies used to determine the unique redshifts of the planes. + + Returns + ------- + The list of unique redshifts of the planes. + """ + return tracer_util.plane_redshifts_from( + galaxies=self.galaxies_ascending_redshift + ) + + @property + def planes(self) -> List[ag.Galaxies]: + """ + Returns a list of list of galaxies grouped into their planes, where planes contained all galaxies at the same + unique redshift. + + Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of + redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the + same plane. + + If the plane redshifts are not input, the redshifts of the galaxies are used to determine the unique redshifts of + the planes. + + For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of + list of galaxies would be [[g1, g2], g3]]. + + Parameters + ---------- + galaxies + The list of galaxies used to determine the unique redshifts of the planes. + plane_redshifts + The redshifts of the planes, which are used to group the galaxies into their respective planes. If not input, + the redshifts of the galaxies are used to determine the unique redshifts of the planes. + + Returns + ------- + The list of list of galaxies grouped into their planes. + """ + return tracer_util.planes_from( + galaxies=self.galaxies_ascending_redshift, + plane_redshifts=self.plane_redshifts, + ) + + @classmethod + def sliced_tracer_from( + cls, + lens_galaxies: List[ag.Galaxy], + line_of_sight_galaxies: List[ag.Galaxy], + source_galaxies: List[ag.Galaxy], + planes_between_lenses: List[int], + cosmology: ag.cosmo.LensingCosmology = None, + ): + """ + Returns a tracer where the lens system is split into planes with specified redshift distances between them. + + This is used for ray-tracing systems with many galaxies at different redshifts (e.g. hundreds or more). If + each galaxy redshift is treated indepedently, this would require many planes to be created, and the multi-plane + ray-tracing calculation would be computationally slow. + + To speed the calculation up, the galaxies are grouped into planes with redshifts separated by the inputs. + To achieve this, the galaxies have their redshifts reassigned from their original values to the nearest + value of a sliced plane redshift. This ensures that every galaxy is in a subset of planes. + + The redshifts of the planes are determines as follows: + + - Use the redshifts of the lens galaxies to determine the redshifts of the planes, where a lens galaxy is + expected to have a large mass and thus contribute to a significant portion of the overall lensing. This ensures + the main lens galaxies have a redshift and plane to themselves, ensuring calculation accuracy. + + - Use the redshift of the source galaxies to determine the redshift of the source plane, ensuring the source + galaxies also have a dedicated redshift and plane for calculation accuracy. + + - Create N planes between Earth and the first lens galaxy, the lens galaxy and the next lens galaxy (and so on) + up to the source galaxy. The number of planes between each set of galaxies is specified by the input + `planes_between_lenses`, where for a lens / source system `planes_between_lenses=[2,3]` would mean there are 2 + planes between Earth and the lens galaxy and 3 planes between the lens and source galaxy. + + - The `line_of_sight_galaxies` are placed in the planes corresponding to their closest redshift. + + Parameters + ---------- + lens_galaxies + The lens galaxies in the ray-tracing calculation. Most use cases will have only one lens galaxy, but the + API supports multiple lens galaxies (e.g. double Einstein ring systems). + line_of_sight_galaxies + The galaxies in the line-of-sight to the primary lens galaxy, which may have many different redshifts + and therefore create computational expensive multi-plane ray-tracing calculations without the plane + grouping provided by this method. + source_galaxies + The source galaxies in the ray-tracing calculation. The API only supports one source galaxy (input multiple + lens galaxies to build a multi-plane system). + planes_between_lenses + The number of slices between each main plane. The first entry in this list determines the number of slices + between Earth (redshift 0.0) and the first lens galaxy, the next between the lens and source, etc. + cosmology + The cosmology used to perform ray-tracing calculations. + """ + cosmology = cosmology or ag.cosmo.Planck15() + + lens_redshifts = tracer_util.plane_redshifts_from(galaxies=lens_galaxies) + + plane_redshifts = tracer_util.ordered_plane_redshifts_with_slicing_from( + lens_redshifts=lens_redshifts, + planes_between_lenses=planes_between_lenses, + source_plane_redshift=source_galaxies[0].redshift, + ) + + plane_redshifts.append(source_galaxies[0].redshift) + + galaxies = lens_galaxies + line_of_sight_galaxies + source_galaxies + + for galaxy in galaxies: + redshift_differences = list( + map(lambda z: abs(z - galaxy.redshift), plane_redshifts) + ) + galaxy.redshift = plane_redshifts[ + redshift_differences.index(min(redshift_differences)) + ] + + return Tracer(galaxies=galaxies, cosmology=cosmology) + + @property + def total_planes(self) -> int: + return len(self.plane_redshifts) + + @aa.decorators.to_grid + def traced_grid_2d_list_from( + self, grid: aa.type.Grid2DLike, xp=np, plane_index_limit: int = Optional[None] + ) -> List[aa.type.Grid2DLike]: + """ + Returns a ray-traced grid of 2D Cartesian (y,x) coordinates which accounts for multi-plane ray-tracing. + + This uses the redshifts and mass profiles of the galaxies contained within the tracer to perform the multi-plane + ray-tracing calculation. + + This function returns a list of 2D (y,x) grids, corresponding to each redshift in the input list of planes. The + plane redshifts are determined from the redshifts of the galaxies in each plane, whereby there is a unique plane + at each redshift containing all galaxies at the same redshift. + + For example, if the `planes` list contains three lists of galaxies with `redshift`'s z0.5, z=1.0 and z=2.0, the + returned list of traced grids will contain three entries corresponding to the input grid after ray-tracing to + redshifts 0.5, 1.0 and 2.0. + + An input cosmology object can change the cosmological model, which is used to compute the scaling + factors between planes (which are derived from their redshifts and angular diameter distances). It is these + scaling factors that account for multi-plane ray tracing effects. + + The calculation can be terminated early by inputting a `plane_index_limit`. All planes whose integer indexes are + above this value are omitted from the calculation and not included in the returned list of grids (the size of + this list is reduced accordingly). + + For example, if `planes` has 3 lists of galaxies, but `plane_index_limit=1`, the third plane (corresponding to + index 2) will not be calculated. The `plane_index_limit` is used to avoid uncessary ray tracing calculations + of higher redshift planes whose galaxies do not have mass profile (and only have light profiles). + + see `autolens.lens.tracer.tracer_util.traced_grid_2d_list_from()` for the full calculation. + + Parameters + ---------- + grid + The 2D (y, x) coordinates on which multi-plane ray-tracing calculations are performed. + plane_index_limit + The integer index of the last plane which is used to perform ray-tracing, all planes with an index above + this value are omitted. + + Returns + ------- + traced_grid_list + A list of 2D (y,x) grids each of which are the input grid ray-traced to a redshift of the input list of + planes. + """ + + grid_2d_list = tracer_util.traced_grid_2d_list_from( + planes=self.planes, + grid=grid, + cosmology=self.cosmology, + plane_index_limit=plane_index_limit, + xp=xp, + ) + + if isinstance(grid, aa.Grid2D): + grid_2d_over_sampled_list = tracer_util.traced_grid_2d_list_from( + planes=self.planes, + grid=grid.over_sampled, + cosmology=self.cosmology, + plane_index_limit=plane_index_limit, + xp=xp, + ) + + grid_2d_new_list = [] + + for i in range(len(grid_2d_list)): + grid_2d_new = aa.Grid2D( + values=grid_2d_list[i], + mask=grid.mask, + over_sampled=grid_2d_over_sampled_list[i], + over_sample_size=grid.over_sample_size, + over_sampler=grid.over_sampler, + ) + + grid_2d_new_list.append(grid_2d_new) + + return grid_2d_new_list + + return grid_2d_list + + def grid_2d_at_redshift_from( + self, grid: aa.type.Grid2DLike, redshift: float + ) -> aa.type.Grid2DLike: + """ + Returns a ray-traced grid of 2D Cartesian (y,x) coordinates, which accounts for multi-plane ray-tracing, at a + specified input redshift which may be different to the redshifts of all planes. + + Given a list of galaxies whose redshifts define a multi-plane lensing system and an input grid of (y,x) arc-second + coordinates (e.g. an image-plane grid), ray-trace the grid to an input redshift in of the multi-plane system. + + This is performed using multi-plane ray-tracing and a list of galaxies which are converted into a list of planes + at a set of redshift. The galaxy mass profiles are used to compute deflection angles. Any redshift can be input + even if a plane does not exist there, including redshifts before the first plane of the lens system. + + An input cosmology object can change the cosmological model, which is used to compute the scaling + factors between planes (which are derived from their redshifts and angular diameter distances). It is these + scaling factors that account for multi-plane ray tracing effects. + + There are two ways the calculation may be performed: + + 1) If the input redshift is the same as the redshift of a plane in the multi-plane system, the grid is ray-traced + to that plane and the traced grid returned. + + 2) If the input redshift is not the same as the redshift of a plane in the multi-plane system, a plane is inserted + at this redshift and the grid is ray-traced to this plane. + + For example, the input list `galaxies` may contained three `Galaxy` objects at redshifts z=0.5, z=1.0 and z=2.0. + We can input an image-plane grid and request that its coordinates are ray-traced to a plane at z=1.75 in this + multi-plane system. This will insert a plane at z=1.75 and use the galaxy's at z=0.5 and z=1.0 to compute + deflection angles, alongside accounting for multi-plane lensing effects via the angular diameter distances + between the different galaxy redshifts. + + Parameters + ---------- + redshift + The redshift the input (image-plane) grid is traced too. + galaxies + A list of galaxies which make up a multi-plane strong lens ray-tracing system. + grid + The 2D (y, x) coordinates which is ray-traced to the input redshift. + cosmology + The cosmology used for ray-tracing from which angular diameter distances between planes are computed. + """ + return tracer_util.grid_2d_at_redshift_from( + redshift=redshift, + galaxies=self.galaxies_ascending_redshift, + grid=grid, + cosmology=self.cosmology, + ) + + @property + def upper_plane_index_with_light_profile(self) -> int: + """ + Returns the index of the highest redshift plane in the tracer which has a light profile. + + When computing the image of a tracer, we only need to trace rays to the highest redshift plane which has a + light profile. This upper index is therefore used to do this, and ensure faster computation by avoiding + ray-tracing to planes which do not have light profiles. + + Returns + ------- + The index of the highest redshift plane in the tracer which has a light profile. + """ + return max( + [ + ( + plane_index + if any([galaxy.has(cls=ag.LightProfile) for galaxy in galaxies]) + else 0 + ) + for (plane_index, galaxies) in enumerate(self.planes) + ] + ) + + def image_2d_list_from( + self, + grid: aa.type.Grid2DLike, + xp=np, + operated_only: Optional[bool] = None, + ) -> List[aa.Array2D]: + """ + Returns a list of the 2D images for each plane from a 2D grid of Cartesian (y,x) coordinates. + + The image of each plane is computed by ray-tracing the grid using te mass profiles of each galaxies and then + summing the images of all galaxies in that plane. If a plane has no galaxies, or if the galaxies in a plane + has no light profiles, a numpy array of zeros is returned. + + For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies + at redshifts z=0.5 and z=1.0 have light and mass profiles, the returned list of images will be the image of the + galaxies at z=0.5 and z=1.0, where the image at redshift z=1.0 will include the lensing effects of the galaxies + at z=0.5. The image at redshift z=2.0 will be a numpy array of zeros. + + The `plane_index` input is used to return a specific image of a plane, as opposed to a list of images + of all planes. This can save on computational time when only the image of a specific plane is needed, + and is used to perform iterative over-sampling calculations. + + The images output by this function do not include instrument operations, such as PSF convolution (for imaging + data) or a Fourier transform (for interferometer data). + + Inherited methods in the `autogalaxy.operate.image` package can apply these operations to the images. + These functions may have the `operated_only` input passed to them, which is why this function includes + the `operated_only` input. + + If the `operated_only` input is included, the function omits light profiles which are parents of + the `LightProfileOperated` object, which signifies that the light profile represents emission that has + already had the instrument operations (e.g. PSF convolution, a Fourier transform) applied to it and therefore + that operation is not performed again. + + See the `autogalaxy.profiles.light` package for details of how images are computed from a light + profile. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where values of the image are evaluated. + operated_only + The returned list from this function contains all light profile images, and they are never operated on + (e.g. via the imaging PSF). However, inherited methods in the `autogalaxy.operate.image` package can + apply these operations to the images, which may have the `operated_only` input passed to them. This input + therefore is used to pass the `operated_only` input to these methods. + """ + + traced_grid_list = self.traced_grid_2d_list_from( + grid=grid, + xp=xp, + plane_index_limit=self.upper_plane_index_with_light_profile, + ) + + image_2d_list = [] + + for plane_index in range(len(traced_grid_list)): + galaxies = self.planes[plane_index] + + image_2d = sum( + [ + galaxy.image_2d_from( + grid=traced_grid_list[plane_index], + operated_only=operated_only, + xp=xp, + ) + for galaxy in galaxies + ] + ) + + image_2d_list.append(image_2d) + + if self.upper_plane_index_with_light_profile < self.total_planes - 1: + if isinstance(grid, aa.Grid2D): + image_2d = aa.Array2D( + values=np.zeros(shape=grid.shape[0]), mask=grid.mask + ) + else: + image_2d = aa.ArrayIrregular(values=np.zeros(grid.shape[0])) + + for plane_index in range( + self.upper_plane_index_with_light_profile, self.total_planes - 1 + ): + image_2d_list.append(image_2d) + + return image_2d_list + + @aa.decorators.to_array + def image_2d_from( + self, + grid: aa.type.Grid2DLike, + xp=np, + operated_only: Optional[bool] = None, + ) -> aa.Array2D: + """ + Returns the 2D image of this ray-tracing strong lens system from a 2D grid of Cartesian (y,x) coordinates. + + This function first computes the image of each plane in the tracer, via the function `image_2d_list_from`. The + images are then summed to give the overall image of the tracer. + + Refer to the function `image_2d_list_from` for a full description of the calculation and how the `operated_only` + input is used. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where values of the image are evaluated. + operated_only + The returned list from this function contains all light profile images, and they are never operated on + (e.g. via the imaging PSF). However, inherited methods in the `autogalaxy.operate.image` package can + apply these operations to the images, which may have the `operated_only` input passed to them. This input + therefore is used to pass the `operated_only` input to these methods. + """ + return sum( + self.image_2d_list_from(grid=grid, operated_only=operated_only, xp=xp) + ) + + def image_2d_via_input_plane_image_from( + self, + grid: aa.type.Grid2DLike, + plane_image: aa.Array2D, + xp=np, + plane_index: int = -1, + include_other_planes: bool = True, + ) -> aa.Array2D: + """ + Returns the lensed image of a plane or galaxy, where the input image is uniform and interpolated to compute + the lensed image. + + The typical use case is inputting the image of an irregular galaxy in the source-plane (whose values are + on a uniform array) and using this function computing the lensed image of this source galaxy. + + By default, this function computes the lensed image of the final plane, which is the source-plane, by using + `plane_index=-1`. For multi-plane lens systems, the lensed image of any planes can be computed by setting + `plane_index` to the index of the plane in the lens system. + + The emission of all other planes and galaxies can be included or omitted setting the `include_other_planes` + bool. If there are multiple planes in a multi-plane lens system, the emission of the other planes are fully + lensed. + + __Source Plane Interpolation__ + + We use the scipy interpolation function `griddata` to create the lensed source galaxy image. + + In brief, we trace light rays to the source plane and calculate values based on where those light rays land in + the source plane via interpolation. + + In more detail: + + - `points`: The 2D grid of (y,x) coordinates representing the location of every pixel of the source galaxy + image in the source-plane, from which we are creating the lensed source image. These coordinates are the + uniform source-plane grid computed after interpolating the irregular mesh the original source reconstruction + used. + + - `values`: The intensity values of the source galaxy image which is used to create the lensed source image. + These values are the flux values of the interpolated source galaxy image computed after interpolating the + irregular mesh the original source reconstruction used. + + - `xi`: The image-plane grid ray traced to the source-plane. This evaluates the flux of each image-plane + lensed source-pixel by ray-tracing it to the source-plane grid and computing its value by interpolating the + source galaxy image. + + Parameters + ---------- + grid + The image-plane grid which is traced to the plane where the image is computed, where these values are + used to perform the interpolation. + plane_image + The image of the plane or galaxy which is interpolated to compute the lensed image. + plane_index + The index of the plane the image is computed, where the default (-1) computes the image in the last plane + and therefore the source-plane. + + Returns + ------- + The lensed image of the plane or galaxy computed by interpolating its image to the image-plane. + """ + + plane_grid = aa.Grid2D.uniform( + shape_native=plane_image.shape_native, + pixel_scales=plane_image.pixel_scales, + ) + + traced_grid = self.traced_grid_2d_list_from( + grid=grid, plane_index_limit=plane_index, xp=xp + )[plane_index] + + image = griddata( + points=plane_grid.array, + values=plane_image.array, + xi=traced_grid.over_sampled.array, + fill_value=0.0, + method="linear", + ) + + if isinstance(grid, aa.Grid2D): + image = grid.over_sampler.binned_array_2d_from(array=image, xp=xp) + + if include_other_planes: + image_list = self.image_2d_list_from(grid=grid, xp=xp, operated_only=False) + + if plane_index < 0: + plane_index = self.total_planes + plane_index + + for plane_lp_index in range(self.total_planes): + if plane_lp_index != plane_index: + image += image_list[plane_lp_index] + + return aa.Array2D(values=image, mask=grid.mask, xp=xp) + + def galaxy_image_2d_dict_from( + self, grid: aa.type.Grid2DLike, xp=np, operated_only: Optional[bool] = None + ) -> Dict[ag.Galaxy, np.ndarray]: + """ + Returns a dictionary associating every `Galaxy` object in the `Tracer` with its corresponding 2D image, using + the instance of each galaxy as the dictionary keys. + + This object is used for adaptive-features, which use the image of each galaxy in a model-fit in order to + adapt quantities like a pixelization or regularization scheme to the surface brightness of the galaxies being + fitted. + + By inheriting from `OperateImageGalaxies` functions which apply operations of this dictionary are accessible, + for example convolving every image with a PSF or applying a Fourier transform to create a galaxy-visibilities + dictionary. + + Parameters + ---------- + grid + The 2D (y,x) coordinates of the (masked) grid, in its original geometric reference frame. + + Returns + ------- + A dictionary associated every galaxy in the tracer with its corresponding 2D image. + """ + + galaxy_image_2d_dict = dict() + + traced_grid_list = self.traced_grid_2d_list_from(grid=grid, xp=xp) + + for plane_index, galaxies in enumerate(self.planes): + image_2d_list = [ + galaxy.image_2d_from( + grid=traced_grid_list[plane_index], + operated_only=operated_only, + xp=xp, + ) + for galaxy in galaxies + ] + + for galaxy_index, galaxy in enumerate(galaxies): + galaxy_image_2d_dict[galaxy] = image_2d_list[galaxy_index] + + return galaxy_image_2d_dict + + @aa.decorators.to_vector_yx + def deflections_yx_2d_from( + self, grid: aa.type.Grid2DLike, xp=np + ) -> Union[aa.VectorYX2D, aa.VectorYX2DIrregular]: + """ + Returns the 2D deflection angles of all galaxies in the tracer, from the image-plane to the source-plane, + accounting for multi-plane ray tracing and from a 2D grid of Cartesian (y,x) coordinates. + + The multi-plane ray tracing calculations are performed in the function `traced_2d_grid_list_from` and its + sub-functions in the `tracer_util` module. This includes performing recursive ray-tracing between planes + based on the planes redshifts and using the cosmological distances between them to scale the deflection angles. + Users should refer to these functions for details on how the ray-tracing is performed. + + This function simply computes the corresponding multi-plane deflection angles by subtracting the image-plane + grid (e.g. before lensing) from the source-plane grid (e.g. after lensing). + + If there is only one plane in the tracer, the deflections are computed by summation of the deflections of all + galaxies in that plane. This is identical too, but computationally faster than, using the multi-plane + ray-tracing calculation. + + See the `autogalaxy.profiles.mass` package for details of how deflections are computed from a mass profile. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where values of the deflections are evaluated. + """ + if self.total_planes > 1: + return self.deflections_between_planes_from(grid=grid, xp=xp) + return self.deflections_of_planes_summed_from(grid=grid, xp=xp) + + @aa.decorators.to_vector_yx + def deflections_of_planes_summed_from( + self, grid: aa.type.Grid2DLike, xp=np + ) -> Union[aa.VectorYX2D, aa.VectorYX2DIrregular]: + """ + Returns the summed 2D deflections angles of all galaxies in the tracer, not accounting for multi-plane ray + tracing, from a 2D grid of Cartesian (y,x) coordinates. + + The deflections of each plane is computed by summing the deflections of all galaxies in that plane. If a + plane has no galaxies, or if the galaxies in a plane has no mass profiles, a numpy array of zeros is returned. + + This calculation does not account for multi-plane ray-tracing effects, it is simply the sum of the deflections + of all galaxies. The function `deflections_between_planes_from` performs the calculation whilst + accounting for multi-plane ray-tracing effects. + + For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies + at redshifts z=0.5 and z=1.0 have mass profiles, the returned deflections will be the sum of the deflections + of the galaxies at z=0.5 and z=1.0. + + The deflections of a tracer do not depend on ray-tracing between grids. This is why the deflections of the + tracer is the sum of the deflections of all planes, and does not need to account for multi-plane ray-tracing + effects (in the way that deflection angles and images do). + + See the `autogalaxy.profiles.mass` package for details of how deflections are computed from a mass profile. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where values of the deflections are evaluated. + """ + return sum( + [ + galaxy.deflections_yx_2d_from(grid=grid, xp=xp) + for galaxy in self.galaxies + ] + ) + + @aa.decorators.to_vector_yx + def deflections_between_planes_from( + self, grid: aa.type.Grid2DLike, xp=np, plane_i=0, plane_j=-1 + ) -> Union[aa.VectorYX2D, aa.VectorYX2DIrregular]: + """ + Returns the summed 2D deflections angles between two input planes in the tracer, accounting for multi-plane + ray tracing, from a 2D grid of Cartesian (y,x) coordinates. + + The multi-plane ray tracing calculations are performed in the function `traced_2d_grid_list_from` and its + sub-functions in the `tracer_util` module. This includes performing recursive ray-tracing between planes + based on the planes redshifts and using the cosmological distances between them to scale the deflection angles. + Users should refer to these functions for details on how the ray-tracing is performed. + + This function simply computes the corresponding multi-plane deflection angles by subtracting the grid + of index `plane_i` to that of index `plane_j`. The default inputs subtract the image-plane grid `plane_i=0` + (e.g. before lensing) from the source-plane grid `plane_j=-1` (e.g. after lensing). + + See the `autogalaxy.profiles.mass` package for details of how deflections are computed from a mass profile. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where values of the deflections are evaluated. + """ + + traced_grids_list = self.traced_grid_2d_list_from(grid=grid, xp=xp) + + return traced_grids_list[plane_i] - traced_grids_list[plane_j] + + @aa.decorators.to_array + def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np) -> aa.Array2D: + """ + Returns the summed 2D convergence of all galaxies in the tracer from a 2D grid of Cartesian (y,x) coordinates. + + The convergence of each plane is computed by summing the convergences of all galaxies in that plane. If a + plane has no galaxies, or if the galaxies in a plane has no mass profiles, a numpy array of zeros is returned. + + For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies + at redshifts z=0.5 and z=1.0 have mass profiles, the returned convergence will be the sum of the convergences + of the galaxies at z=0.5 and z=1.0. + + The convergences of a tracer do not depend on ray-tracing between grids. This is why the convergence of the + tracer is the sum of the convergences of all planes, and does not need to account for multi-plane ray-tracing + effects (in the way that deflection angles and images do). + + See the `autogalaxy.profiles.mass` package for details of how convergences are computed from a mass profile. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where values of the convergence are evaluated. + """ + return sum( + [galaxy.convergence_2d_from(grid=grid, xp=xp) for galaxy in self.galaxies] + ) + + @aa.decorators.to_array + def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np) -> aa.Array2D: + """ + Returns the summed 2D potential of all galaxies in the tracer from a 2D grid of Cartesian (y,x) coordinates. + + The potential of each plane is computed by summing the potentials of all galaxies in that plane. If a + plane has no galaxies, or if the galaxies in a plane has no mass profiles, a numpy array of zeros is returned. + + For example, if the tracer's planes contain galaxies at redshifts z=0.5, z=1.0 and z=2.0, and the galaxies + at redshifts z=0.5 and z=1.0 have mass profiles, the returned potential will be the sum of the potentials + of the galaxies at z=0.5 and z=1.0. + + The potentials of a tracer do not depend on ray-tracing between grids. This is why the potential of the + tracer is the sum of the potentials of all planes, and does not need to account for multi-plane ray-tracing + effects (in the way that deflection angles and images do). + + See the `autogalaxy.profiles.mass` package for details of how potentials are computed from a mass profile. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where values of the potential are evaluated. + """ + return sum( + [galaxy.potential_2d_from(grid=grid, xp=xp) for galaxy in self.galaxies] + ) + + @aa.decorators.to_array + def time_delays_from(self, grid: aa.type.Grid2DLike, xp=np) -> aa.Array2D: + """ + Returns the gravitational lensing time delay in days, for a grid of 2D (y, x) coordinates. + + This function calculates the time delay at each image-plane position due to both geometric and gravitational + (Shapiro) effects, as described by the Fermat potential, which are computed using the deflection angles of the + galaxies in the lens system. + + Time dleays are computed from a reference point, which is the delay one would compute without a mass model. + This means a time delay could be negative, which means the light travel time is faster when lensing is + accounted for. When performing fitting, all time-delays are subtracted by the time-delay of the + shortest time-delay, such that its value is zero and all other time-delays are positive. + + A full description of the calculation is given in the `autolens.lens.tracer.tracer_util.time_delays_from` + function, which performs the calculation and has full latex documentation of the equations used. + """ + return tracer_util.time_delays_from( + galaxies=ag.Galaxies(self.galaxies_ascending_redshift), + grid=grid, + xp=xp, + cosmology=self.cosmology, + ) + + def has(self, cls: Type) -> bool: + """ + Returns a bool specifying whether this tracer has a galaxy with a certain class type. + + For example, for the input `cls=ag.LightProfile`, this function returns True if any galaxy in the tracer has a + light profile and false if no galaxy has a light profile. + + This function is used to check for mass profiles and specific types of profiles, like the linear light profile. + + Parameters + ---------- + cls + The class type of the galaxy which is checked for in the tracer. + + Returns + ------- + True if any galaxy in the tracer has the input class type, else False. + """ + return any(map(lambda galaxy: galaxy.has(cls=cls), self.galaxies)) + + def cls_list_from(self, cls: Type) -> List: + """ + Returns a list of objects in the tracer which are an instance of the input `cls`. + + For example: + + - If the input is `cls=ag.LightProfile`, a list containing all light profiles in the tracer is returned. + + Returns + ------- + The list of objects in the tracer that inherit from input `cls`. + """ + cls_list = [] + + for galaxy in self.galaxies: + if galaxy.has(cls=cls): + for cls_galaxy in galaxy.cls_list_from(cls=cls): + cls_list.append(cls_galaxy) + + return cls_list + + def plane_index_via_redshift_from(self, redshift: float) -> Optional[int]: + """ + Returns the index of a plane at a given redshift. + + This is used to determine the index of a plane in the tracer, which is useful for multi-plane ray-tracing + calculations. The index of the plane may, for example, be used to extract grid of a specific plane in the + tracer after multi-plane ray-tracing calculations. + + A tolerance of 1e-8 is used to determine if the input redshift is close to the redshift of a plane. If + no matching plane is found, None is returned. + + Parameters + ---------- + redshift + The redshift of the plane to find the index of. + + Returns + ------- + The index of the plane that matches the input redshift. + """ + + for plane_index, plane_redshift in enumerate(self.plane_redshifts): + if np.isclose(redshift, plane_redshift, atol=1e-8): + return plane_index + + @property + def plane_indexes_with_pixelizations(self) -> List[int]: + """ + Returns a list of integer indexes of the indexes of planes which use a `Pixelization` to reconstruct the + source galaxy. + + This list is used to set up an inversion, whereby each pixelization is extracted from the tracer with its + corresponding ray-traced grid and passed to the PyAutoArray `inversion` module. + + Returns + ------- + The list of integer indexes of the planes which use a `Pixelization` to reconstruct the source galaxy. + """ + plane_indexes_with_inversions = [ + plane_index if plane.has(cls=aa.Pixelization) else None + for (plane_index, plane) in enumerate(self.planes) + ] + return [ + plane_index + for plane_index in plane_indexes_with_inversions + if plane_index is not None + ] + + @property + def plane_indexes_with_images(self): + """ + Returns a list of integer indexes of the indexes of planes which create an image, meaning they either + have a `LightProfile` or `Pixelization`. + + This list is used to visualize double source plane lenses, whereby a fit for every plane with a + `LightProfile` or `Pixelization` is created. + + Returns + ------- + The list of integer indexes of the planes which create an image. + """ + plane_indexes_with_images = [ + plane_index if plane.has(cls=ag.LightProfile) else None + for (plane_index, plane) in enumerate(self.planes) + ] + self.plane_indexes_with_pixelizations + + plane_indexes_with_images = list(dict.fromkeys(plane_indexes_with_images)) + + return [ + plane_index + for plane_index in plane_indexes_with_images + if plane_index is not None + ] + + @property + def perform_inversion(self) -> bool: + """ + Returns a bool specifying whether this fit object performs an inversion. + + This is based on whether any of the galaxies have a `Pixelization` or `LightProfileLinear` object, in which + case an inversion is performed. + + Returns + ------- + A bool which is True if an inversion is performed. + """ + return any(plane.perform_inversion for plane in self.planes) + + def extract_attribute( + self, cls: Type, attr_name: str, filter_nones: Optional[bool] = False + ) -> List[Union[aa.ArrayIrregular, aa.Grid2DIrregular]]: + """ + Returns an extracted attribute of a class in the tracer as a `ValueIrregular` or `Grid2DIrregular` object. + + For example, if a tracer has a galaxy with two light profiles, the input: + + `tracer.extract_attribute(cls=LightProfile, name="axis_ratio")` + + Returns + + `ArrayIrregular(values=[axis_ratio_0, axis_ratio_1])` + + If the image plane has two galaxies with two mass profiles and the source plane another galaxy with a + mass profile, the input: + + `tracer.extract_attribute(cls=MassProfile, name="centre")` + + Returns + + GridIrregular2D(grid=[(centre_y_0, centre_x_0), (centre_y_1, centre_x_1), (centre_y_2, centre_x_2)]) + + The primary use of this function is to extract the attributes of profiles for visualization, for example + plotting the centres of all mass profiles colored by their profile over the tracer's image. + + Parameters + ---------- + cls + The class type of object whose attribute is extracted (e.g. light profile, mass profile). + attr_name + The name of the attribute which is extracted from the class type (e.g. axis_ratio, centre). + """ + + def extract(value, name): + try: + return getattr(value, name) + except (AttributeError, IndexError): + return None + + attributes = [ + extract(value, attr_name) + for galaxy in self.galaxies + for value in galaxy.__dict__.values() + if isinstance(value, cls) + ] + + if attributes == []: + return None + elif isinstance(attributes[0], float): + return aa.ArrayIrregular(values=attributes) + elif isinstance(attributes[0], tuple): + return aa.Grid2DIrregular(values=attributes) + + def extract_attributes_of_planes( + self, cls: Type, attr_name: str, filter_nones: Optional[bool] = False + ) -> List[Union[aa.ArrayIrregular, aa.Grid2DIrregular]]: + """ + Returns an extracted attribute of a class in the tracer as a list of `ValueIrregular` or `Grid2DIrregular` + objects, where the indexes of the list correspond to the tracer's planes. + + For example, if a tracer has an image-plane with a galaxy with a light profile and a source-plane with a galaxy + with a light profile, the input: + + `tracer.extract_attributes_of_planes(cls=LightProfile, name="axis_ratio")` + + Returns: + + [ArrayIrregular(values=[axis_ratio_0]), ArrayIrregular(values=[axis_ratio_1])] + + If the image plane has two galaxies with a mass profile each and the source plane another galaxy with a + mass profile, input: + + `tracer.extract_attributes_of_planes(cls=MassProfile, name="centres")` + + Returns: + + [ + Grid2DIrregular(values=[(centre_y_0, centre_x_0)]), + Grid2DIrregular(values=[(centre_y_0, centre_x_0), (centre_y_1, centre_x_1)]) + ] + + If a profile does not have a certain entry, it is replaced with a None. The Nones can be removed + by setting `filter_nones=True`. + + The primary use of this function is to extract the attributes of profiles for visualization, for example + plotting the centres of all mass profiles colored by their profile over the tracer's image. + + Parameters + ---------- + cls + The class type of object whose attribute is extracted (e.g. light profile, mass profile). + attr_name + The name of the attribute which is extracted from the class type (e.g. axis_ratio, centre). + filter_nones + If True, None entries are removed from the list. + """ + + attribute_list = [] + + for plane in self.planes: + for galaxy in plane: + attribute_list += [ + galaxy.extract_attribute(cls=cls, attr_name=attr_name) + ] + + if filter_nones: + return list(filter(None, attribute_list)) + + return attribute_list + + def extract_attributes_of_galaxies( + self, cls: Type, attr_name: str, filter_nones: Optional[bool] = False + ) -> List[Union[aa.ArrayIrregular, aa.Grid2DIrregular]]: + """ + Returns an attribute of a class in the tracer as a list of `ValueIrregular` or `Grid2DIrregular` objects, where + the indexes of the list correspond to the tracer's galaxies. If a plane has multiple galaxies it will have a + list with each galaxy as an entry. + + For example, if a tracer has an image-plane with a galaxy with a light profile and a source-plane with a galaxy + with a light profile, the input: + + `tracer.extract_attributes_of_galaxies(cls=LightProfile, name="axis_ratio")` + + Returns: + + [ArrayIrregular(values=[axis_ratio_0]), ArrayIrregular(values=[axis_ratio_1])] + + If the image plane has two galaxies with a mass profile each and the source plane another galaxy with a + mass profile, the input: + + `tracer.extract_attributes_of_galaxies(cls=MassProfile, name="centres")` + + Returns: + + [ + Grid2DIrregular(values=[(centre_y_0, centre_x_0)]), + Grid2DIrregular(values=[(centre_y_0, centre_x_0)]) + Grid2DIrregular(values=[(centre_y_0, centre_x_0)]) + ] + + If the first galaxy in the image plane in the example above had two mass profiles as well as the galaxy in the + source plane it would return: + + [ + Grid2DIrregular(values=[(centre_y_0, centre_x_0), (centre_y_1, centre_x_1)]), + Grid2DIrregular(values=[(centre_y_0, centre_x_0)]) + Grid2DIrregular(values=[(centre_y_0, centre_x_0, (centre_y_1, centre_x_1))]) + ] + + If a profile does not have a certain entry, it is replaced with a None. The Nones can be removed + by setting `filter_nones=True`. + + The primary use of this function is to extract the attributes of profiles for visualization, for example + plotting the centres of all mass profiles colored by their profile over the tracer's image. + + Parameters + ---------- + cls + The class type of object whose attribute is extracted (e.g. light profile, mass profile). + attr_name + The name of the attribute which is extracted from the class type (e.g. axis_ratio, centre). + filter_nones + """ + if filter_nones: + return [ + galaxy.extract_attribute(cls=cls, attr_name=attr_name) + for galaxy in self.galaxies + if galaxy.extract_attribute(cls=cls, attr_name=attr_name) is not None + ] + + else: + return [ + galaxy.extract_attribute(cls=cls, attr_name=attr_name) + for galaxy in self.galaxies + ] + + def extract_profile(self, profile_name: str) -> GeometryProfile: + """ + Returns a profile (e.g. a `LightProfile`, `MassProfile`, `Point`) from the tracer using the name of that + component. + + For example, if a tracer has two galaxies named `lens` and `source`, where `lens` has a light profile + named `light_0` and `source` has a light profile named `light_1`, the input: + + `tracer.extract_profile(profile_name="light_1")` + + Return the light profile of the source galaxy. + + This primarily used for point-source modeling, where the locations that the point-sources tracer to in + different planes must be paired to their corresponding point-source `Point` profile. + + Parameters + ---------- + profile_name + The name of the profile component in the tracer. + """ + for galaxy in self.galaxies: + try: + return galaxy.__dict__[profile_name] + except KeyError: + pass + + def extract_plane_index_of_profile(self, profile_name: str) -> int: + """ + Returns the plane index of a profile (e.g. a `LightProfile`, `MassProfile`, `Point`) from the tracer using + the name of that component. + + For example, if a tracer has two galaxies named `lens` and `source`, where `lens` has a light profile + named `light_0` and `source` has a light profile named `light_1`, the input: + + `tracer.extract_profile(profile_name="light_1")` + + Would return `plane_index=1` corresponding to the profile in the source plane. + + This primarily used for point-source modeling, where the locations that the point-sources tracer to in + different planes must be paired to their corresponding point-source `Point` profile. + + Parameters + ---------- + profile_name + The name of the profile component in the tracer. + """ + for plane_index, galaxies in enumerate(self.planes): + for galaxy in galaxies: + if profile_name in galaxy.__dict__: + return plane_index + + def set_snr_of_snr_light_profiles( + self, + grid: aa.type.Grid2DLike, + exposure_time: float, + background_sky_level: float = 0.0, + psf: Optional[aa.Convolver] = None, + ): + """ + Iterate over every `LightProfileSNR` in the tracer and set their `intensity` values to values which give + their input `signal_to_noise_ratio` value, which is performed as follows: + + - Evaluate the image of each light profile on the input grid. + - Blur this image with a PSF, if included. + - Take the value of the brightest pixel. + - Use an input `exposure_time` and `background_sky` (e.g. from the `SimulatorImaging` object) to determine + what value of `intensity` gives the desired signal to noise ratio for the image. + + The intensity is set using an input grid, meaning that for strong lensing calculations the ray-traced grid + can be used such that the S/N accounts for the magnification of a source galaxy. + + Parameters + ---------- + grid + The (y, x) coordinates in the original reference frame of the grid. + exposure_time + The exposure time of the simulated imaging. + background_sky_level + The level of the background sky of the simulated imaging. + psf + The psf of the simulated imaging which can change the S/N of the light profile due to spreading out + the emission. + """ + has_snr_profile = any( + isinstance(light_profile, LightProfileSNR) + for galaxies in self.planes + for galaxy in galaxies + for light_profile in galaxy.cls_list_from(cls=ag.LightProfile) + ) + if not has_snr_profile: + return + + grid = aa.Grid2D.uniform( + shape_native=grid.shape_native, + pixel_scales=grid.pixel_scales, + ) + + traced_grids_of_planes_list = self.traced_grid_2d_list_from(grid=grid) + + for plane_index, galaxies in enumerate(self.planes): + for galaxy in galaxies: + for light_profile in galaxy.cls_list_from(cls=ag.LightProfile): + if isinstance(light_profile, LightProfileSNR): + light_profile.set_intensity_from( + grid=traced_grids_of_planes_list[plane_index], + exposure_time=exposure_time, + background_sky_level=background_sky_level, + psf=psf, + ) diff --git a/autolens/lens/tracer_util.py b/autolens/lens/tracer_util.py index 541495d6a..fb3b24c22 100644 --- a/autolens/lens/tracer_util.py +++ b/autolens/lens/tracer_util.py @@ -1,546 +1,546 @@ -""" -Utility functions supporting the ``Tracer`` ray-tracing calculations. - -This module contains lower-level helpers that are called by ``Tracer`` but kept separate -to avoid cluttering the main class. Key functions: - -- ``plane_redshifts_from`` — derives the list of unique plane redshifts from a list of - galaxies, collapsing multiple galaxies at the same redshift into a single plane. -- ``ordered_plane_redshifts_with_slicing_from`` — extends the above with optional - redshift slicing for multi-plane calculations that include intermediate planes. -- ``positions_in_ordered_planes_from`` — distributes a set of image-plane positions - across the ordered plane list so that multi-plane tracing can propagate them. -""" -import numpy as np -from typing import List, Optional - -import autoarray as aa -import autogalaxy as ag -import autogalaxy.plot as aplt - -from autolens import exc - - -def _redshift_is_traced(redshift) -> bool: - """ - Return True if ``redshift`` is a JAX traced scalar that cannot be coerced to a - Python float without raising under ``jax.jit``. - - Galaxy redshifts are normally Python ``float`` / ``int`` values, but when a - ``af.UniformPrior`` is bound to a ``Galaxy.redshift`` field (e.g. for a free- - parameter subhalo redshift; see PyAutoLens issue #498), the value passed in at - likelihood-evaluation time becomes a traced scalar under ``jax.jit``. Most - sort-and-compare helpers in this module need to fall back to a JAX-aware path - in that case rather than calling ``sorted`` / ``float()`` / ``<=`` on the value. - """ - if isinstance(redshift, (int, float)): - return False - if isinstance(redshift, np.ndarray) and redshift.shape == (): - return False - try: - float(redshift) - except Exception: - return True - return False - - -def _any_traced(galaxies: List[ag.Galaxy]) -> bool: - return any(_redshift_is_traced(g.redshift) for g in galaxies) - - -def plane_redshifts_from(galaxies: List[ag.Galaxy]) -> List[float]: - """ - Returns a list of plane redshifts from a list of galaxies, using the redshifts of the galaxies to determine the - unique redshifts of the planes. - - Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of - redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the - same plane. - - For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of - redshifts would be [1.0, 2.0]. - - When one or more galaxies have a JAX-traced redshift (e.g. a free-parameter - subhalo redshift under ``jax.jit``), the function cannot Python-sort or - ``float()``-coerce the values. It instead walks the input list in order, - deduplicating *concrete* redshifts only and treating each traced redshift as a - unique plane at its input position. The caller must pass galaxies in - ascending-redshift order in this case (which ``af.Collection(galaxies=...)`` - naturally does when the user declares them as ``lens, subhalo, source``). - - Parameters - ---------- - galaxies - The list of galaxies used to determine the unique redshifts of the planes. - - Returns - ------- - The list of unique redshifts of the planes. - """ - - if not _any_traced(galaxies): - galaxies_ascending_redshift = sorted(galaxies, key=lambda galaxy: galaxy.redshift) - - # Coerce to float to avoid issues with other float types not being hashable. - plane_redshifts = [float(galaxy.redshift) for galaxy in galaxies_ascending_redshift] - - return list(dict.fromkeys(plane_redshifts)) - - plane_redshifts: List = [] - seen_concrete: set = set() - for galaxy in galaxies: - z = galaxy.redshift - if _redshift_is_traced(z): - plane_redshifts.append(z) - else: - zf = float(z) - if zf not in seen_concrete: - seen_concrete.add(zf) - plane_redshifts.append(zf) - - return plane_redshifts - - -def planes_from( - galaxies: List[ag.Galaxy], plane_redshifts: Optional[List[float]] = None -) -> List[ag.Galaxies]: - """ - Returns a list of list of galaxies grouped into their planes, where planes contained all galaxies at the same - unique redshift. - - Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of - redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the - same plane. - - If the plane redshifts are not input, the redshifts of the galaxies are used to determine the unique redshifts of - the planes. - - For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of - list of galaxies would be [[g1, g2], g3]]. - - When any galaxy has a JAX-traced redshift, planes are built by walking the - input galaxies in order and grouping by *concrete* redshift equality only; - each traced-redshift galaxy gets its own dedicated plane in input position. - See ``plane_redshifts_from`` for the matching rule and assumption on input - ordering. - - Parameters - ---------- - galaxies - The list of galaxies used to determine the unique redshifts of the planes. - plane_redshifts - The redshifts of the planes, which are used to group the galaxies into their respective planes. If not input, - the redshifts of the galaxies are used to determine the unique redshifts of the planes. - - Returns - ------- - The list of list of galaxies grouped into their planes. - """ - - if not _any_traced(galaxies): - galaxies_ascending_redshift = sorted(galaxies, key=lambda galaxy: galaxy.redshift) - - if plane_redshifts is None: - plane_redshifts = plane_redshifts_from(galaxies=galaxies_ascending_redshift) - - planes = [[] for i in range(len(plane_redshifts))] - - for galaxy in galaxies_ascending_redshift: - index = (np.abs(np.asarray(plane_redshifts) - galaxy.redshift)).argmin() - planes[index].append(galaxy) - - for index in range(len(planes)): - planes[index] = ag.Galaxies(galaxies=planes[index]) - - return planes - - plane_groups: List = [] # list of (key, [galaxies]) - for galaxy in galaxies: - z = galaxy.redshift - if _redshift_is_traced(z): - plane_groups.append((z, [galaxy])) - else: - zf = float(z) - for i, (key, _) in enumerate(plane_groups): - if not _redshift_is_traced(key) and float(key) == zf: - plane_groups[i][1].append(galaxy) - break - else: - plane_groups.append((zf, [galaxy])) - - return [ag.Galaxies(galaxies=group) for _, group in plane_groups] - - -def traced_grid_2d_list_from( - planes: List[List[ag.Galaxy]], - grid: aa.type.Grid2DLike, - cosmology: ag.cosmo.LensingCosmology = None, - plane_index_limit: int = Optional[None], - xp=np, -): - """ - Returns a ray-traced grid of 2D Cartesian (y,x) coordinates which accounts for multi-plane ray-tracing. - - This uses the redshifts and mass profiles of the galaxies contained within the tracer to perform the multi-plane - ray-tracing calculation. - - This function returns a list of 2D (y,x) grids, corresponding to each redshift in the input list of planes. The - plane redshifts are determined from the redshifts of the galaxies in each plane, whereby there is a unique plane - at each redshift containing all galaxies at the same redshift. - - For example, if the `planes` list contains three lists of galaxies with `redshift`'s z0.5, z=1.0 and z=2.0, the - returned list of traced grids will contain three entries corresponding to the input grid after ray-tracing to - redshifts 0.5, 1.0 and 2.0. - - An input cosmology object can change the cosmological model, which is used to compute the scaling - factors between planes (which are derived from their redshifts and angular diameter distances). It is these - scaling factors that account for multi-plane ray tracing effects. - - The calculation can be terminated early by inputting a `plane_index_limit`. All planes whose integer indexes are - above this value are omitted from the calculation and not included in the returned list of grids (the size of - this list is reduced accordingly). - - For example, if `planes` has 3 lists of galaxies, but `plane_index_limit=1`, the third plane (corresponding to - index 2) will not be calculated. The `plane_index_limit` is used to avoid uncessary ray tracing calculations - of higher redshift planes whose galaxies do not have mass profile (and only have light profiles). - - Parameters - ---------- - galaxies - The galaxies whose mass profiles are used to perform multi-plane ray-tracing, where the list of galaxies - has an index for each plane, correspond to each unique redshift in the multi-plane system. - grid - The 2D (y, x) coordinates on which multi-plane ray-tracing calculations are performed. - cosmology - The cosmology used for ray-tracing from which angular diameter distances between planes are computed. - plane_index_limit - The integer index of the last plane which is used to perform ray-tracing, all planes with an index above - this value are omitted. - - Returns - ------- - traced_grid_list - A list of 2D (y,x) grids each of which are the input grid ray-traced to a redshift of the input list of planes. - """ - cosmology = cosmology or ag.cosmo.Planck15() - - traced_grid_list = [] - traced_deflection_list = [] - - redshift_list = [galaxies[0].redshift for galaxies in planes] - - for plane_index, galaxies in enumerate(planes): - - scaled_grid = xp.asarray(grid.array) - - if plane_index > 0: - - for previous_plane_index in range(plane_index): - scaling_factor = cosmology.scaling_factor_between_redshifts_from( - redshift_0=redshift_list[previous_plane_index], - redshift_1=galaxies[0].redshift, - redshift_final=redshift_list[-1], - xp=xp, - ) - - scaled_deflections = ( - scaling_factor * traced_deflection_list[previous_plane_index].array - ) - - scaled_grid = scaled_grid - scaled_deflections - - scaled_grid = aa.Grid2DIrregular( - values=scaled_grid, xp=xp - ) - - traced_grid_list.append(scaled_grid) - - if plane_index_limit is not None: - if plane_index == plane_index_limit: - return traced_grid_list - - deflections_yx_2d = sum( - (g.deflections_yx_2d_from(grid=scaled_grid, xp=xp) for g in galaxies) - ) - - traced_deflection_list.append(deflections_yx_2d) - - return traced_grid_list - - -def grid_2d_at_redshift_from( - redshift: float, - galaxies: List[ag.Galaxy], - grid: aa.type.Grid2DLike, - cosmology: ag.cosmo.LensingCosmology = None, - xp=np, -) -> aa.type.Grid2DLike: - """ - Returns a ray-traced grid of 2D Cartesian (y,x) coordinates, which accounts for multi-plane ray-tracing, at a - specified input redshift which may be different to the redshifts of all planes. - - Given a list of galaxies whose redshifts define a multi-plane lensing system and an input grid of (y,x) arc-second - coordinates (e.g. an image-plane grid), ray-trace the grid to an input redshift in of the multi-plane system. - - This is performed using multi-plane ray-tracing and a list of galaxies which are converted into a list of planes - at a set of redshift. The galaxy mass profiles are used to compute deflection angles. Any redshift can be input - even if a plane does not exist there, including redshifts before the first plane of the lens system. - - An input cosmology object can change the cosmological model, which is used to compute the scaling - factors between planes (which are derived from their redshifts and angular diameter distances). It is these - scaling factors that account for multi-plane ray tracing effects. - - There are two ways the calculation may be performed: - - 1) If the input redshift is the same as the redshift of a plane in the multi-plane system, the grid is ray-traced - to that plane and the traced grid returned. - - 2) If the input redshift is not the same as the redshift of a plane in the multi-plane system, a plane is inserted - at this redshift and the grid is ray-traced to this plane. - - For example, the input list `galaxies` may contained three `ag.Galaxy` objects at redshifts z=0.5, z=1.0 and z=2.0. - We can input an image-plane grid and request that its coordinates are ray-traced to a plane at z=1.75 in this - multi-plane system. This will insert a plane at z=1.75 and use the galaxy's at z=0.5 and z=1.0 to compute - deflection angles, alongside accounting for multi-plane lensing effects via the angular diameter distances - between the different galaxy redshifts. - - Parameters - ---------- - redshift - The redshift the input (image-plane) grid is traced too. - galaxies - A list of galaxies which make up a multi-plane strong lens ray-tracing system. - grid - The 2D (y, x) coordinates which is ray-traced to the input redshift. - cosmology - The cosmology used for ray-tracing from which angular diameter distances between planes are computed. - """ - cosmology = cosmology or ag.cosmo.Planck15() - - if _redshift_is_traced(redshift) or _any_traced(galaxies): - # JAX path: the requested redshift always matches the redshift of one of - # the input galaxies (this is how AnalysisLens.tracer_via_instance_from - # invokes the function — it passes ``redshift=instance.galaxies.subhalo. - # redshift`` and the subhalo galaxy is in ``galaxies`` too). So we just - # need to identify which plane that galaxy lives in (via Python identity, - # not value comparison) and return the traced grid at that plane. - planes = planes_from(galaxies=galaxies) - - plane_index_match = None - for plane_index, plane_galaxies in enumerate(planes): - for plane_galaxy in plane_galaxies: - if plane_galaxy.redshift is redshift: - plane_index_match = plane_index - break - if plane_index_match is not None: - break - - if plane_index_match is None: - raise exc.RayTracingException( - "grid_2d_at_redshift_from was called under JAX with a traced " - "redshift that does not match any galaxy in the input list by " - "Python identity. The current implementation only supports the " - "case where the requested redshift is the same object as one of " - "the galaxy redshifts (e.g. instance.galaxies.subhalo.redshift). " - "Insertion at an arbitrary traced redshift is not yet supported." - ) - - traced_grid_list = traced_grid_2d_list_from( - planes=planes, grid=grid, cosmology=cosmology, xp=xp - ) - - return traced_grid_list[plane_index_match] - - plane_redshifts = plane_redshifts_from(galaxies=galaxies) - - if redshift <= plane_redshifts[0]: - return grid.copy() - - planes = planes_from(galaxies=galaxies, plane_redshifts=plane_redshifts) - - plane_index_with_redshift = [ - plane_index - for plane_index, galaxies in enumerate(planes) - if galaxies[0].redshift == redshift - ] - - if plane_index_with_redshift: - traced_grid_list = traced_grid_2d_list_from( - planes=planes, grid=grid, cosmology=cosmology, xp=xp - ) - - return traced_grid_list[plane_index_with_redshift[0]] - - for plane_index, plane_redshift in enumerate(plane_redshifts): - if redshift > plane_redshift: - plane_index_insert = plane_index + 1 - - planes.insert(plane_index_insert, [ag.Galaxy(redshift=redshift)]) - - traced_grid_list = traced_grid_2d_list_from( - planes=planes, grid=grid, cosmology=cosmology, xp=xp - ) - - return traced_grid_list[plane_index_insert] - - -def time_delays_from( - galaxies: List[ag.Galaxy], - grid: aa.type.Grid2DLike, - xp=np, - cosmology: ag.cosmo.LensingCosmology = None, -) -> aa.type.Grid2DLike: - r""" - Returns the gravitational lensing time delay in days for a grid of 2D (y, x) coordinates. - - This function calculates the time delay at each image-plane position due to both geometric and gravitational - (Shapiro) effects, as described by the Fermat potential, which are computed using the deflection angles of the - galaxies in the lens system. - - It requires a two-plane system (lens and source), and does not currently support multi-plane time delay - calculations involving more than two planes, but it could be extended to do so in the future. - - The time delay is computed as: - - .. math:: - \Delta t(\boldsymbol{\theta}) = \frac{D_{\Delta t}}{c} \, \phi(\boldsymbol{\theta}) - - where: - - - \( \boldsymbol{\theta} \): image-plane coordinate - - \( \phi(\boldsymbol{\theta}) \): Fermat potential at each coordinate - - \( c \): speed of light - - \( D_{\Delta t} \): time-delay distance - - The time-delay distance is given by: - - .. math:: - D_{\Delta t} = (1 + z_l) \frac{D_d D_s}{D_{ds}} - - with \( D_d, D_s, D_{ds} \) the angular diameter distances to the lens, to the source, and from lens to source. - - The time delay is computed using the Fermat potential, as described by the equations above. - - An input cosmology object can change the cosmological model, which is used to compute the scaling - factors between planes (which are derived from their redshifts and angular diameter distances). It is these - scaling factors that account for multi-plane ray tracing effects. - - Parameters - ---------- - galaxies - List of galaxies whose mass profiles define the lens and source planes. Must contain exactly two redshifts. - grid - The 2D (y, x) image-plane coordinates where the time delay is computed. - cosmology - The cosmological model used to calculate angular diameter distances. Defaults to Planck15. - - Returns - ------- - The time delay at each (y, x) coordinate in the input grid, in units of days. - """ - cosmology = cosmology or ag.cosmo.Planck15() - - plane_redshifts = plane_redshifts_from(galaxies=galaxies) - - if len(plane_redshifts) != 2: - raise exc.RayTracingException( - "The time delay calculation requires exactly two planes, but the input galaxies have " - f"{len(plane_redshifts)} planes with redshifts {plane_redshifts}." - ) - - z_l, z_s = plane_redshifts[0], plane_redshifts[1] - - # ----------------- - # Constants (SI) - # ----------------- - kpc_in_m = xp.asarray(3.085677581491367e19) # kpc in meters - arcsec_to_rad = xp.pi / 648000.0 # arcsec -> rad (pi / (180*3600)) - seconds_per_day = xp.asarray(86400.0) - c = xp.asarray(299792458.0) # m/s - - # This factor converts Fermat potential in arcsec^2 into days once multiplied by D_dt/c - factor = (arcsec_to_rad * arcsec_to_rad) / seconds_per_day - - # ----------------- - # Angular diameter distances (kpc) - # ----------------- - Dd_kpc = cosmology.angular_diameter_distance_to_earth_in_kpc_from(z_l, xp=xp) - Ds_kpc = cosmology.angular_diameter_distance_to_earth_in_kpc_from(z_s, xp=xp) - Dds_kpc = cosmology.angular_diameter_distance_between_redshifts_in_kpc_from( - redshift_0=z_l, redshift_1=z_s, xp=xp - ) - - # Time-delay distance in meters: (1+z_l) * Dd * Ds / Dds - D_dt_m = (1.0 + z_l) * (Dd_kpc * Ds_kpc / Dds_kpc) * kpc_in_m - - # Fermat potential (should be in arcsec^2 for this formula) - import autogalaxy as ag - - fermat_potential = ag.LensCalc.from_mass_obj(galaxies).fermat_potential_from( - grid=grid, xp=xp - ) - - # Final time delay in days - return (D_dt_m / c) * fermat_potential * factor - - -def ordered_plane_redshifts_with_slicing_from( - lens_redshifts, planes_between_lenses, source_plane_redshift -): - """ - Given a set of lens plane redshifts, the source-plane redshift and the number of planes between each, setup the \ - plane redshifts using these values. A lens redshift corresponds to the 'main' lens galaxy(s), - whereas the slices collect line-of-sight halos over a range of redshifts. - - The source-plane redshift is removed from the ordered plane redshifts that are returned, so that galaxies are not \ - planed at the source-plane redshift. - - For example, if the main plane redshifts are [1.0, 2.0], and the bin sizes are [1,3], the following redshift - slices for planes will be used:: - - z=0.5 - z=1.0 - z=1.25 - z=1.5 - z=1.75 - z=2.0 - - Parameters - ---------- - lens_redshifts : [float] - The redshifts of the main-planes (e.g. the lens galaxy), which determine where redshift intervals are placed. - planes_between_lenses : [int] - The number of slices between each main plane. The first entry in this list determines the number of slices \ - between Earth (redshift 0.0) and main plane 0, the next between main planes 0 and 1, etc. - source_plane_redshift - The redshift of the source-plane, which is input explicitly to ensure galaxies are not placed in the \ - source-plane. - """ - - # Check that the number of slices between lens planes is equal to the number of intervals between the lens planes. - if len(lens_redshifts) != len(planes_between_lenses) - 1: - raise exc.PlaneException( - "The number of lens_plane_redshifts input is not equal to the number of " - "slices_between_lens_planes+1." - ) - - plane_redshifts = [] - - # Add redshift 0.0 and the source plane redshifit to the lens plane redshifts, so that calculation below can use - # them when dividing slices. These will be removed by the return function at the end from the plane redshifts. - - lens_redshifts.insert(0, 0.0) - lens_redshifts.append(source_plane_redshift) - - for lens_plane_index in range(1, len(lens_redshifts)): - previous_plane_redshift = lens_redshifts[lens_plane_index - 1] - plane_redshift = lens_redshifts[lens_plane_index] - slice_total = planes_between_lenses[lens_plane_index - 1] - plane_redshifts += list( - np.linspace(previous_plane_redshift, plane_redshift, slice_total + 2) - )[1:] - - return plane_redshifts[0:-1] - - - +""" +Utility functions supporting the ``Tracer`` ray-tracing calculations. + +This module contains lower-level helpers that are called by ``Tracer`` but kept separate +to avoid cluttering the main class. Key functions: + +- ``plane_redshifts_from`` — derives the list of unique plane redshifts from a list of + galaxies, collapsing multiple galaxies at the same redshift into a single plane. +- ``ordered_plane_redshifts_with_slicing_from`` — extends the above with optional + redshift slicing for multi-plane calculations that include intermediate planes. +- ``positions_in_ordered_planes_from`` — distributes a set of image-plane positions + across the ordered plane list so that multi-plane tracing can propagate them. +""" +import numpy as np +from typing import List, Optional + +import autoarray as aa +import autogalaxy as ag +import autogalaxy.plot as aplt + +from autolens import exc + + +def _redshift_is_traced(redshift) -> bool: + """ + Return True if ``redshift`` is a JAX traced scalar that cannot be coerced to a + Python float without raising under ``jax.jit``. + + Galaxy redshifts are normally Python ``float`` / ``int`` values, but when a + ``af.UniformPrior`` is bound to a ``Galaxy.redshift`` field (e.g. for a free- + parameter subhalo redshift; see PyAutoLens issue #498), the value passed in at + likelihood-evaluation time becomes a traced scalar under ``jax.jit``. Most + sort-and-compare helpers in this module need to fall back to a JAX-aware path + in that case rather than calling ``sorted`` / ``float()`` / ``<=`` on the value. + """ + if isinstance(redshift, (int, float)): + return False + if isinstance(redshift, np.ndarray) and redshift.shape == (): + return False + try: + float(redshift) + except Exception: + return True + return False + + +def _any_traced(galaxies: List[ag.Galaxy]) -> bool: + return any(_redshift_is_traced(g.redshift) for g in galaxies) + + +def plane_redshifts_from(galaxies: List[ag.Galaxy]) -> List[float]: + """ + Returns a list of plane redshifts from a list of galaxies, using the redshifts of the galaxies to determine the + unique redshifts of the planes. + + Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of + redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the + same plane. + + For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of + redshifts would be [1.0, 2.0]. + + When one or more galaxies have a JAX-traced redshift (e.g. a free-parameter + subhalo redshift under ``jax.jit``), the function cannot Python-sort or + ``float()``-coerce the values. It instead walks the input list in order, + deduplicating *concrete* redshifts only and treating each traced redshift as a + unique plane at its input position. The caller must pass galaxies in + ascending-redshift order in this case (which ``af.Collection(galaxies=...)`` + naturally does when the user declares them as ``lens, subhalo, source``). + + Parameters + ---------- + galaxies + The list of galaxies used to determine the unique redshifts of the planes. + + Returns + ------- + The list of unique redshifts of the planes. + """ + + if not _any_traced(galaxies): + galaxies_ascending_redshift = sorted(galaxies, key=lambda galaxy: galaxy.redshift) + + # Coerce to float to avoid issues with other float types not being hashable. + plane_redshifts = [float(galaxy.redshift) for galaxy in galaxies_ascending_redshift] + + return list(dict.fromkeys(plane_redshifts)) + + plane_redshifts: List = [] + seen_concrete: set = set() + for galaxy in galaxies: + z = galaxy.redshift + if _redshift_is_traced(z): + plane_redshifts.append(z) + else: + zf = float(z) + if zf not in seen_concrete: + seen_concrete.add(zf) + plane_redshifts.append(zf) + + return plane_redshifts + + +def planes_from( + galaxies: List[ag.Galaxy], plane_redshifts: Optional[List[float]] = None +) -> List[ag.Galaxies]: + """ + Returns a list of list of galaxies grouped into their planes, where planes contained all galaxies at the same + unique redshift. + + Each plane redshift corresponds to a unique redshift in the list of galaxies, such that the returned list of + redshifts contains no duplicate values. This means multiple galaxies at the same redshift are assigned to the + same plane. + + If the plane redshifts are not input, the redshifts of the galaxies are used to determine the unique redshifts of + the planes. + + For example, if the input is three galaxies, two at redshift 1.0 and one at redshift 2.0, the returned list of + list of galaxies would be [[g1, g2], g3]]. + + When any galaxy has a JAX-traced redshift, planes are built by walking the + input galaxies in order and grouping by *concrete* redshift equality only; + each traced-redshift galaxy gets its own dedicated plane in input position. + See ``plane_redshifts_from`` for the matching rule and assumption on input + ordering. + + Parameters + ---------- + galaxies + The list of galaxies used to determine the unique redshifts of the planes. + plane_redshifts + The redshifts of the planes, which are used to group the galaxies into their respective planes. If not input, + the redshifts of the galaxies are used to determine the unique redshifts of the planes. + + Returns + ------- + The list of list of galaxies grouped into their planes. + """ + + if not _any_traced(galaxies): + galaxies_ascending_redshift = sorted(galaxies, key=lambda galaxy: galaxy.redshift) + + if plane_redshifts is None: + plane_redshifts = plane_redshifts_from(galaxies=galaxies_ascending_redshift) + + planes = [[] for i in range(len(plane_redshifts))] + + for galaxy in galaxies_ascending_redshift: + index = (np.abs(np.asarray(plane_redshifts) - galaxy.redshift)).argmin() + planes[index].append(galaxy) + + for index in range(len(planes)): + planes[index] = ag.Galaxies(galaxies=planes[index]) + + return planes + + plane_groups: List = [] # list of (key, [galaxies]) + for galaxy in galaxies: + z = galaxy.redshift + if _redshift_is_traced(z): + plane_groups.append((z, [galaxy])) + else: + zf = float(z) + for i, (key, _) in enumerate(plane_groups): + if not _redshift_is_traced(key) and float(key) == zf: + plane_groups[i][1].append(galaxy) + break + else: + plane_groups.append((zf, [galaxy])) + + return [ag.Galaxies(galaxies=group) for _, group in plane_groups] + + +def traced_grid_2d_list_from( + planes: List[List[ag.Galaxy]], + grid: aa.type.Grid2DLike, + cosmology: ag.cosmo.LensingCosmology = None, + plane_index_limit: int = Optional[None], + xp=np, +): + """ + Returns a ray-traced grid of 2D Cartesian (y,x) coordinates which accounts for multi-plane ray-tracing. + + This uses the redshifts and mass profiles of the galaxies contained within the tracer to perform the multi-plane + ray-tracing calculation. + + This function returns a list of 2D (y,x) grids, corresponding to each redshift in the input list of planes. The + plane redshifts are determined from the redshifts of the galaxies in each plane, whereby there is a unique plane + at each redshift containing all galaxies at the same redshift. + + For example, if the `planes` list contains three lists of galaxies with `redshift`'s z0.5, z=1.0 and z=2.0, the + returned list of traced grids will contain three entries corresponding to the input grid after ray-tracing to + redshifts 0.5, 1.0 and 2.0. + + An input cosmology object can change the cosmological model, which is used to compute the scaling + factors between planes (which are derived from their redshifts and angular diameter distances). It is these + scaling factors that account for multi-plane ray tracing effects. + + The calculation can be terminated early by inputting a `plane_index_limit`. All planes whose integer indexes are + above this value are omitted from the calculation and not included in the returned list of grids (the size of + this list is reduced accordingly). + + For example, if `planes` has 3 lists of galaxies, but `plane_index_limit=1`, the third plane (corresponding to + index 2) will not be calculated. The `plane_index_limit` is used to avoid uncessary ray tracing calculations + of higher redshift planes whose galaxies do not have mass profile (and only have light profiles). + + Parameters + ---------- + galaxies + The galaxies whose mass profiles are used to perform multi-plane ray-tracing, where the list of galaxies + has an index for each plane, correspond to each unique redshift in the multi-plane system. + grid + The 2D (y, x) coordinates on which multi-plane ray-tracing calculations are performed. + cosmology + The cosmology used for ray-tracing from which angular diameter distances between planes are computed. + plane_index_limit + The integer index of the last plane which is used to perform ray-tracing, all planes with an index above + this value are omitted. + + Returns + ------- + traced_grid_list + A list of 2D (y,x) grids each of which are the input grid ray-traced to a redshift of the input list of planes. + """ + cosmology = cosmology or ag.cosmo.Planck15() + + traced_grid_list = [] + traced_deflection_list = [] + + redshift_list = [galaxies[0].redshift for galaxies in planes] + + for plane_index, galaxies in enumerate(planes): + + scaled_grid = xp.asarray(grid.array) + + if plane_index > 0: + + for previous_plane_index in range(plane_index): + scaling_factor = cosmology.scaling_factor_between_redshifts_from( + redshift_0=redshift_list[previous_plane_index], + redshift_1=galaxies[0].redshift, + redshift_final=redshift_list[-1], + xp=xp, + ) + + scaled_deflections = ( + scaling_factor * traced_deflection_list[previous_plane_index].array + ) + + scaled_grid = scaled_grid - scaled_deflections + + scaled_grid = aa.Grid2DIrregular( + values=scaled_grid, xp=xp + ) + + traced_grid_list.append(scaled_grid) + + if plane_index_limit is not None: + if plane_index == plane_index_limit: + return traced_grid_list + + deflections_yx_2d = sum( + (g.deflections_yx_2d_from(grid=scaled_grid, xp=xp) for g in galaxies) + ) + + traced_deflection_list.append(deflections_yx_2d) + + return traced_grid_list + + +def grid_2d_at_redshift_from( + redshift: float, + galaxies: List[ag.Galaxy], + grid: aa.type.Grid2DLike, + cosmology: ag.cosmo.LensingCosmology = None, + xp=np, +) -> aa.type.Grid2DLike: + """ + Returns a ray-traced grid of 2D Cartesian (y,x) coordinates, which accounts for multi-plane ray-tracing, at a + specified input redshift which may be different to the redshifts of all planes. + + Given a list of galaxies whose redshifts define a multi-plane lensing system and an input grid of (y,x) arc-second + coordinates (e.g. an image-plane grid), ray-trace the grid to an input redshift in of the multi-plane system. + + This is performed using multi-plane ray-tracing and a list of galaxies which are converted into a list of planes + at a set of redshift. The galaxy mass profiles are used to compute deflection angles. Any redshift can be input + even if a plane does not exist there, including redshifts before the first plane of the lens system. + + An input cosmology object can change the cosmological model, which is used to compute the scaling + factors between planes (which are derived from their redshifts and angular diameter distances). It is these + scaling factors that account for multi-plane ray tracing effects. + + There are two ways the calculation may be performed: + + 1) If the input redshift is the same as the redshift of a plane in the multi-plane system, the grid is ray-traced + to that plane and the traced grid returned. + + 2) If the input redshift is not the same as the redshift of a plane in the multi-plane system, a plane is inserted + at this redshift and the grid is ray-traced to this plane. + + For example, the input list `galaxies` may contained three `ag.Galaxy` objects at redshifts z=0.5, z=1.0 and z=2.0. + We can input an image-plane grid and request that its coordinates are ray-traced to a plane at z=1.75 in this + multi-plane system. This will insert a plane at z=1.75 and use the galaxy's at z=0.5 and z=1.0 to compute + deflection angles, alongside accounting for multi-plane lensing effects via the angular diameter distances + between the different galaxy redshifts. + + Parameters + ---------- + redshift + The redshift the input (image-plane) grid is traced too. + galaxies + A list of galaxies which make up a multi-plane strong lens ray-tracing system. + grid + The 2D (y, x) coordinates which is ray-traced to the input redshift. + cosmology + The cosmology used for ray-tracing from which angular diameter distances between planes are computed. + """ + cosmology = cosmology or ag.cosmo.Planck15() + + if _redshift_is_traced(redshift) or _any_traced(galaxies): + # JAX path: the requested redshift always matches the redshift of one of + # the input galaxies (this is how AnalysisLens.tracer_via_instance_from + # invokes the function — it passes ``redshift=instance.galaxies.subhalo. + # redshift`` and the subhalo galaxy is in ``galaxies`` too). So we just + # need to identify which plane that galaxy lives in (via Python identity, + # not value comparison) and return the traced grid at that plane. + planes = planes_from(galaxies=galaxies) + + plane_index_match = None + for plane_index, plane_galaxies in enumerate(planes): + for plane_galaxy in plane_galaxies: + if plane_galaxy.redshift is redshift: + plane_index_match = plane_index + break + if plane_index_match is not None: + break + + if plane_index_match is None: + raise exc.RayTracingException( + "grid_2d_at_redshift_from was called under JAX with a traced " + "redshift that does not match any galaxy in the input list by " + "Python identity. The current implementation only supports the " + "case where the requested redshift is the same object as one of " + "the galaxy redshifts (e.g. instance.galaxies.subhalo.redshift). " + "Insertion at an arbitrary traced redshift is not yet supported." + ) + + traced_grid_list = traced_grid_2d_list_from( + planes=planes, grid=grid, cosmology=cosmology, xp=xp + ) + + return traced_grid_list[plane_index_match] + + plane_redshifts = plane_redshifts_from(galaxies=galaxies) + + if redshift <= plane_redshifts[0]: + return grid.copy() + + planes = planes_from(galaxies=galaxies, plane_redshifts=plane_redshifts) + + plane_index_with_redshift = [ + plane_index + for plane_index, galaxies in enumerate(planes) + if galaxies[0].redshift == redshift + ] + + if plane_index_with_redshift: + traced_grid_list = traced_grid_2d_list_from( + planes=planes, grid=grid, cosmology=cosmology, xp=xp + ) + + return traced_grid_list[plane_index_with_redshift[0]] + + for plane_index, plane_redshift in enumerate(plane_redshifts): + if redshift > plane_redshift: + plane_index_insert = plane_index + 1 + + planes.insert(plane_index_insert, [ag.Galaxy(redshift=redshift)]) + + traced_grid_list = traced_grid_2d_list_from( + planes=planes, grid=grid, cosmology=cosmology, xp=xp + ) + + return traced_grid_list[plane_index_insert] + + +def time_delays_from( + galaxies: List[ag.Galaxy], + grid: aa.type.Grid2DLike, + xp=np, + cosmology: ag.cosmo.LensingCosmology = None, +) -> aa.type.Grid2DLike: + r""" + Returns the gravitational lensing time delay in days for a grid of 2D (y, x) coordinates. + + This function calculates the time delay at each image-plane position due to both geometric and gravitational + (Shapiro) effects, as described by the Fermat potential, which are computed using the deflection angles of the + galaxies in the lens system. + + It requires a two-plane system (lens and source), and does not currently support multi-plane time delay + calculations involving more than two planes, but it could be extended to do so in the future. + + The time delay is computed as: + + .. math:: + \Delta t(\boldsymbol{\theta}) = \frac{D_{\Delta t}}{c} \, \phi(\boldsymbol{\theta}) + + where: + + - \( \boldsymbol{\theta} \): image-plane coordinate + - \( \phi(\boldsymbol{\theta}) \): Fermat potential at each coordinate + - \( c \): speed of light + - \( D_{\Delta t} \): time-delay distance + + The time-delay distance is given by: + + .. math:: + D_{\Delta t} = (1 + z_l) \frac{D_d D_s}{D_{ds}} + + with \( D_d, D_s, D_{ds} \) the angular diameter distances to the lens, to the source, and from lens to source. + + The time delay is computed using the Fermat potential, as described by the equations above. + + An input cosmology object can change the cosmological model, which is used to compute the scaling + factors between planes (which are derived from their redshifts and angular diameter distances). It is these + scaling factors that account for multi-plane ray tracing effects. + + Parameters + ---------- + galaxies + List of galaxies whose mass profiles define the lens and source planes. Must contain exactly two redshifts. + grid + The 2D (y, x) image-plane coordinates where the time delay is computed. + cosmology + The cosmological model used to calculate angular diameter distances. Defaults to Planck15. + + Returns + ------- + The time delay at each (y, x) coordinate in the input grid, in units of days. + """ + cosmology = cosmology or ag.cosmo.Planck15() + + plane_redshifts = plane_redshifts_from(galaxies=galaxies) + + if len(plane_redshifts) != 2: + raise exc.RayTracingException( + "The time delay calculation requires exactly two planes, but the input galaxies have " + f"{len(plane_redshifts)} planes with redshifts {plane_redshifts}." + ) + + z_l, z_s = plane_redshifts[0], plane_redshifts[1] + + # ----------------- + # Constants (SI) + # ----------------- + kpc_in_m = xp.asarray(3.085677581491367e19) # kpc in meters + arcsec_to_rad = xp.pi / 648000.0 # arcsec -> rad (pi / (180*3600)) + seconds_per_day = xp.asarray(86400.0) + c = xp.asarray(299792458.0) # m/s + + # This factor converts Fermat potential in arcsec^2 into days once multiplied by D_dt/c + factor = (arcsec_to_rad * arcsec_to_rad) / seconds_per_day + + # ----------------- + # Angular diameter distances (kpc) + # ----------------- + Dd_kpc = cosmology.angular_diameter_distance_to_earth_in_kpc_from(z_l, xp=xp) + Ds_kpc = cosmology.angular_diameter_distance_to_earth_in_kpc_from(z_s, xp=xp) + Dds_kpc = cosmology.angular_diameter_distance_between_redshifts_in_kpc_from( + redshift_0=z_l, redshift_1=z_s, xp=xp + ) + + # Time-delay distance in meters: (1+z_l) * Dd * Ds / Dds + D_dt_m = (1.0 + z_l) * (Dd_kpc * Ds_kpc / Dds_kpc) * kpc_in_m + + # Fermat potential (should be in arcsec^2 for this formula) + import autogalaxy as ag + + fermat_potential = ag.LensCalc.from_mass_obj(galaxies).fermat_potential_from( + grid=grid, xp=xp + ) + + # Final time delay in days + return (D_dt_m / c) * fermat_potential * factor + + +def ordered_plane_redshifts_with_slicing_from( + lens_redshifts, planes_between_lenses, source_plane_redshift +): + """ + Given a set of lens plane redshifts, the source-plane redshift and the number of planes between each, setup the \ + plane redshifts using these values. A lens redshift corresponds to the 'main' lens galaxy(s), + whereas the slices collect line-of-sight halos over a range of redshifts. + + The source-plane redshift is removed from the ordered plane redshifts that are returned, so that galaxies are not \ + planed at the source-plane redshift. + + For example, if the main plane redshifts are [1.0, 2.0], and the bin sizes are [1,3], the following redshift + slices for planes will be used:: + + z=0.5 + z=1.0 + z=1.25 + z=1.5 + z=1.75 + z=2.0 + + Parameters + ---------- + lens_redshifts : [float] + The redshifts of the main-planes (e.g. the lens galaxy), which determine where redshift intervals are placed. + planes_between_lenses : [int] + The number of slices between each main plane. The first entry in this list determines the number of slices \ + between Earth (redshift 0.0) and main plane 0, the next between main planes 0 and 1, etc. + source_plane_redshift + The redshift of the source-plane, which is input explicitly to ensure galaxies are not placed in the \ + source-plane. + """ + + # Check that the number of slices between lens planes is equal to the number of intervals between the lens planes. + if len(lens_redshifts) != len(planes_between_lenses) - 1: + raise exc.PlaneException( + "The number of lens_plane_redshifts input is not equal to the number of " + "slices_between_lens_planes+1." + ) + + plane_redshifts = [] + + # Add redshift 0.0 and the source plane redshifit to the lens plane redshifts, so that calculation below can use + # them when dividing slices. These will be removed by the return function at the end from the plane redshifts. + + lens_redshifts.insert(0, 0.0) + lens_redshifts.append(source_plane_redshift) + + for lens_plane_index in range(1, len(lens_redshifts)): + previous_plane_redshift = lens_redshifts[lens_plane_index - 1] + plane_redshift = lens_redshifts[lens_plane_index] + slice_total = planes_between_lenses[lens_plane_index - 1] + plane_redshifts += list( + np.linspace(previous_plane_redshift, plane_redshift, slice_total + 2) + )[1:] + + return plane_redshifts[0:-1] + + + diff --git a/autolens/mock.py b/autolens/mock.py index b591f211c..a6d442e77 100644 --- a/autolens/mock.py +++ b/autolens/mock.py @@ -1,22 +1,22 @@ -import numpy as np - -from autofit.mock import * # noqa -from autoarray.mock import * # noqa -from autogalaxy.mock import * # noqa -from autolens import Tracer - -from autolens.imaging.mock.mock_fit_imaging import MockFitImaging # noqa -from autolens.lens.mock.mock_tracer import MockTracer # noqa -from autolens.lens.mock.mock_tracer import MockTracerPoint # noqa -from autolens.point.mock.mock_solver import MockPointSolver # noqa - - -class NullTracer(Tracer): - def __init__(self): - super().__init__([]) - - def deflections_yx_2d_from(self, grid, xp=np): - return xp.zeros_like(grid.array) - - def deflections_between_planes_from(self, grid, xp=np, plane_i=0, plane_j=-1): - return xp.zeros_like(grid.array) +import numpy as np + +from autofit.mock import * # noqa +from autoarray.mock import * # noqa +from autogalaxy.mock import * # noqa +from autolens import Tracer + +from autolens.imaging.mock.mock_fit_imaging import MockFitImaging # noqa +from autolens.lens.mock.mock_tracer import MockTracer # noqa +from autolens.lens.mock.mock_tracer import MockTracerPoint # noqa +from autolens.point.mock.mock_solver import MockPointSolver # noqa + + +class NullTracer(Tracer): + def __init__(self): + super().__init__([]) + + def deflections_yx_2d_from(self, grid, xp=np): + return xp.zeros_like(grid.array) + + def deflections_between_planes_from(self, grid, xp=np, plane_i=0, plane_j=-1): + return xp.zeros_like(grid.array) diff --git a/autolens/point/dataset.py b/autolens/point/dataset.py index 7d0315853..ecb02a4ab 100644 --- a/autolens/point/dataset.py +++ b/autolens/point/dataset.py @@ -1,403 +1,403 @@ -""" -Data structures for point-source strong lens observations. - -Point-source lensing arises when the background source is compact enough to be treated -as a point (e.g. a quasar, supernova, or compact radio source). Gravitational lensing -splits the source into multiple images whose positions, fluxes, and time delays constrain -the lens mass distribution. - -``PointDataset`` holds the image-plane positions, fluxes, and time delays of a single -named point source together with their noise maps. The ``name`` attribute is used to -pair this dataset with the corresponding ``Point`` model component during fitting. -When multiple point sources are fitted simultaneously (for example many multiply-imaged -background sources in a strong-lens cluster) they are collected in a plain Python -``list`` of ``PointDataset`` objects. - -Two I/O surfaces are supported: - -- JSON (via :func:`autonerves.output_to_json` / :func:`autonerves.from_json`) — exact - round-trip, one file per ``PointDataset``; the canonical modeling input. -- CSV (via :meth:`PointDataset.to_csv` / :meth:`PointDataset.from_csv` and the - module-level :func:`output_to_csv` / :func:`list_from_csv`) — one row per observed - image, grouped by ``name``, so that tens or hundreds of cluster-scale point sources - can be edited in a single spreadsheet. -""" -from autonerves import csvable -from typing import List, Tuple, Optional, Union - -import autoarray as aa - - -_BASE_HEADERS = ["name", "y", "x", "positions_noise"] -_FLUX_HEADERS = ["flux", "flux_noise"] -_TIME_DELAY_HEADERS = ["time_delay", "time_delay_noise"] -_REDSHIFT_HEADERS = ["redshift"] - - -class PointDataset: - def __init__( - self, - name: str, - positions: Union[aa.Grid2DIrregular, List[List], List[Tuple]], - positions_noise_map: Union[float, aa.ArrayIrregular, List[float]], - fluxes: Optional[Union[aa.ArrayIrregular, List[float]]] = None, - fluxes_noise_map: Optional[Union[float, aa.ArrayIrregular, List[float]]] = None, - time_delays: Optional[Union[aa.ArrayIrregular, List[float]]] = None, - time_delays_noise_map: Optional[ - Union[float, aa.ArrayIrregular, List[float]] - ] = None, - redshift: Optional[float] = None, - ): - """ - A collection of the data component that can be used for point-source model-fitting, for example fitting the - observed positions of a a strongly lensed quasar or supernovae or in strong lens cluster modeling, where - there may be many tens or hundreds of individual source galaxies each of which are modeled as a point source. - - The name of the dataset is required for point-source model-fitting, as it pairs a point-source dataset with - its corresponding point-source in the model-fit. For example, if a dataset has the name `source_1`, it will - be paired with the `Point` model-component which has the name `source_1`. If a dataset component is not - successfully paired with a model-component, an error is raised. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` in the `Model`. - positions - The image-plane (y,x) positions of the point-source. - positions_noise_map - The noise-value of every (y,x) position, which is typically the pixel-scale of the data. - fluxes - The image-plane flux of each observed point-source of light. - fluxes_noise_map - The noise-value of every observed flux, which is typically measured from the pixel values of the pixel - containing the point source after convolution with the PSF. - time_delays - The time delays of each observed point-source of light in days. - time_delays_noise_map - The noise-value of every observed time delay, which is typically measured from the time delay analysis. - redshift - The redshift of the source. Optional; when provided it is carried through CSV round-trips alongside - the positions so cluster-scale workflows can encode per-source redshifts in a single spreadsheet. - """ - - self.name = name - - # Ensure positions is a Grid2DIrregular - self.positions = ( - positions - if isinstance(positions, aa.Grid2DIrregular) - else aa.Grid2DIrregular(values=positions) - ) - - # Ensure positions_noise_map is an ArrayIrregular - if isinstance(positions_noise_map, float): - positions_noise_map = [positions_noise_map] * len(self.positions) - - self.positions_noise_map = ( - positions_noise_map - if isinstance(positions_noise_map, aa.ArrayIrregular) - else aa.ArrayIrregular(values=positions_noise_map) - ) - - def convert_to_array_irregular(values): - """ - Convert data to ArrayIrregular if it is not already. - """ - return ( - aa.ArrayIrregular(values=values) - if values is not None and not isinstance(values, aa.ArrayIrregular) - else values - ) - - # Convert fluxes, time delays and their noise maps to ArrayIrregular if provided as values and not already this type - - self.fluxes = convert_to_array_irregular(fluxes) - self.fluxes_noise_map = convert_to_array_irregular(fluxes_noise_map) - self.time_delays = convert_to_array_irregular(time_delays) - self.time_delays_noise_map = convert_to_array_irregular(time_delays_noise_map) - - self.redshift = float(redshift) if redshift is not None else None - - @property - def info(self) -> str: - """ - A dictionary representation of this instance. - - Arrays are represented as lists or lists of lists. - """ - info = f"name : {self.name}\n" - info += f"positions : {self.positions}\n" - info += f"positions_noise_map : {self.positions_noise_map}\n" - info += f"fluxes : {self.fluxes}\n" - info += f"fluxes_noise_map : {self.fluxes_noise_map}\n" - info += f"time_delays : {self.time_delays}\n" - info += f"time_delays_noise_map : {self.time_delays_noise_map}\n" - info += f"redshift : {self.redshift}\n" - return info - - def extent_from(self, buffer: float = 0.1): - y_max = max(self.positions[:, 0]) + buffer - y_min = min(self.positions[:, 0]) - buffer - x_max = max(self.positions[:, 1]) + buffer - x_min = min(self.positions[:, 1]) - buffer - - return [y_min, y_max, x_min, x_max] - - def to_csv(self, file_path: str): - """ - Write this dataset to ``file_path`` as a CSV with one row per image. - - Optional flux / time-delay columns are included only when this dataset carries - the corresponding values. For multi-dataset output use :func:`output_to_csv`. - """ - output_to_csv([self], file_path) - - @classmethod - def from_csv( - cls, file_path: str, name: Optional[str] = None - ) -> "PointDataset": - """ - Load a single ``PointDataset`` from a CSV written by :meth:`to_csv` or - :func:`output_to_csv`. - - Parameters - ---------- - file_path - Path to a CSV file with at minimum the columns - ``name, y, x, positions_noise``. - name - The ``name`` group to load. Must be provided when the CSV contains more - than one ``name``; when the CSV contains exactly one group it is picked - automatically. - """ - datasets = list_from_csv(file_path) - - if not datasets: - raise ValueError( - f"CSV file {file_path!r} contained no PointDataset rows." - ) - - if name is None: - if len(datasets) > 1: - available = [d.name for d in datasets] - raise ValueError( - f"CSV file {file_path!r} contains {len(datasets)} groups " - f"({available!r}); pass name= to select one." - ) - return datasets[0] - - for dataset in datasets: - if dataset.name == name: - return dataset - - available = [d.name for d in datasets] - raise ValueError( - f"CSV file {file_path!r} has no group named {name!r}. " - f"Available groups: {available!r}." - ) - - -def _optional_values(dataset: PointDataset, attr: str) -> Optional[List[float]]: - values = getattr(dataset, attr) - if values is None: - return None - return [float(v) for v in values] - - -def output_to_csv(datasets: List[PointDataset], file_path: str): - """ - Write a list of ``PointDataset`` objects to a single CSV with one row per observed - image. - - The base columns (``name, y, x, positions_noise``) are always written. The - optional ``flux``/``flux_noise``, ``time_delay``/``time_delay_noise`` and - ``redshift`` columns are included when *any* dataset in ``datasets`` carries - those values; datasets that do not carry them leave those cells blank. - - When written, every row in a given ``name`` group repeats the same ``redshift`` - value — the source redshift is a per-source property, not per-image. - - This is the hand-editable / spreadsheet form preferred for strong-lens cluster - workflows with tens or hundreds of multiply-imaged sources. For exact - round-trip serialisation use ``output_to_json`` / ``from_json``. - """ - include_flux = any(d.fluxes is not None for d in datasets) - include_time_delay = any(d.time_delays is not None for d in datasets) - include_redshift = any(d.redshift is not None for d in datasets) - - headers = list(_BASE_HEADERS) - if include_flux: - headers += _FLUX_HEADERS - if include_time_delay: - headers += _TIME_DELAY_HEADERS - if include_redshift: - headers += _REDSHIFT_HEADERS - - rows = [] - for dataset in datasets: - positions = dataset.positions - positions_noise = _optional_values(dataset, "positions_noise_map") - fluxes = _optional_values(dataset, "fluxes") - fluxes_noise = _optional_values(dataset, "fluxes_noise_map") - time_delays = _optional_values(dataset, "time_delays") - time_delays_noise = _optional_values(dataset, "time_delays_noise_map") - - for i in range(len(positions)): - row = { - "name": dataset.name, - "y": float(positions[i][0]), - "x": float(positions[i][1]), - "positions_noise": positions_noise[i], - } - if include_flux: - row["flux"] = "" if fluxes is None else fluxes[i] - row["flux_noise"] = ( - "" if fluxes_noise is None else fluxes_noise[i] - ) - if include_time_delay: - row["time_delay"] = ( - "" if time_delays is None else time_delays[i] - ) - row["time_delay_noise"] = ( - "" if time_delays_noise is None else time_delays_noise[i] - ) - if include_redshift: - row["redshift"] = ( - "" if dataset.redshift is None else dataset.redshift - ) - rows.append(row) - - csvable.output_to_csv(rows, file_path, headers=headers) - - -def _float_column( - group_rows: List[dict], column: str, group_name: str -) -> Optional[List[float]]: - raw = [row.get(column, "") for row in group_rows] - populated = [v for v in raw if v not in ("", None)] - - if not populated: - return None - - if len(populated) != len(raw): - raise ValueError( - f"CSV group {group_name!r} has partially populated column " - f"{column!r}; every row in the group must have a value or all be blank." - ) - - return [float(v) for v in raw] - - -def _group_redshift( - group_rows: List[dict], group_name: str -) -> Optional[float]: - raw = [row.get("redshift", "") for row in group_rows] - populated = [v for v in raw if v not in ("", None)] - - if not populated: - return None - - if len(populated) != len(raw): - raise ValueError( - f"CSV group {group_name!r} has partially populated column " - f"'redshift'; every row in the group must have a value or all be blank." - ) - - values = [float(v) for v in populated] - if any(v != values[0] for v in values): - raise ValueError( - f"CSV group {group_name!r} has inconsistent 'redshift' values " - f"{values!r}; a source redshift must be identical across all of its " - f"image rows." - ) - - return values[0] - - -def list_from_csv(file_path: str) -> List[PointDataset]: - """ - Load a list of ``PointDataset`` objects from a CSV written by - :func:`output_to_csv` (or :meth:`PointDataset.to_csv`). - - Rows are grouped by their ``name`` column — one ``PointDataset`` per distinct - name, preserving the order of first appearance. Optional per-image columns - (``flux``/``flux_noise``, ``time_delay``/``time_delay_noise``) are carried through - per-group: if every row in a group populates the column the values are loaded, - if every row leaves it blank the corresponding attribute is set to ``None``, and - any partial-population is rejected with a ``ValueError``. - - The optional ``redshift`` column is per-source (not per-image): every row within - a group must share the same value. A group with mixed or differing redshifts is - rejected with a ``ValueError``. - """ - rows = csvable.list_from_csv(file_path) - - if not rows: - return [] - - headers = list(rows[0].keys()) - - for required in _BASE_HEADERS: - if required not in headers: - raise ValueError( - f"CSV file {file_path!r} is missing required column {required!r}; " - f"expected headers starting with {_BASE_HEADERS!r}." - ) - - groups: "dict[str, List[dict]]" = {} - for row in rows: - groups.setdefault(row["name"], []).append(row) - - has_flux_column = "flux" in headers - has_flux_noise_column = "flux_noise" in headers - has_time_delay_column = "time_delay" in headers - has_time_delay_noise_column = "time_delay_noise" in headers - has_redshift_column = "redshift" in headers - - datasets: List[PointDataset] = [] - for name, group_rows in groups.items(): - positions = [(float(r["y"]), float(r["x"])) for r in group_rows] - positions_noise_map = [ - float(r["positions_noise"]) for r in group_rows - ] - - fluxes = ( - _float_column(group_rows, "flux", name) - if has_flux_column - else None - ) - fluxes_noise_map = ( - _float_column(group_rows, "flux_noise", name) - if has_flux_noise_column - else None - ) - time_delays = ( - _float_column(group_rows, "time_delay", name) - if has_time_delay_column - else None - ) - time_delays_noise_map = ( - _float_column(group_rows, "time_delay_noise", name) - if has_time_delay_noise_column - else None - ) - redshift = ( - _group_redshift(group_rows, name) - if has_redshift_column - else None - ) - - datasets.append( - PointDataset( - name=name, - positions=positions, - positions_noise_map=positions_noise_map, - fluxes=fluxes, - fluxes_noise_map=fluxes_noise_map, - time_delays=time_delays, - time_delays_noise_map=time_delays_noise_map, - redshift=redshift, - ) - ) - - return datasets +""" +Data structures for point-source strong lens observations. + +Point-source lensing arises when the background source is compact enough to be treated +as a point (e.g. a quasar, supernova, or compact radio source). Gravitational lensing +splits the source into multiple images whose positions, fluxes, and time delays constrain +the lens mass distribution. + +``PointDataset`` holds the image-plane positions, fluxes, and time delays of a single +named point source together with their noise maps. The ``name`` attribute is used to +pair this dataset with the corresponding ``Point`` model component during fitting. +When multiple point sources are fitted simultaneously (for example many multiply-imaged +background sources in a strong-lens cluster) they are collected in a plain Python +``list`` of ``PointDataset`` objects. + +Two I/O surfaces are supported: + +- JSON (via :func:`autonerves.output_to_json` / :func:`autonerves.from_json`) — exact + round-trip, one file per ``PointDataset``; the canonical modeling input. +- CSV (via :meth:`PointDataset.to_csv` / :meth:`PointDataset.from_csv` and the + module-level :func:`output_to_csv` / :func:`list_from_csv`) — one row per observed + image, grouped by ``name``, so that tens or hundreds of cluster-scale point sources + can be edited in a single spreadsheet. +""" +from autonerves import csvable +from typing import List, Tuple, Optional, Union + +import autoarray as aa + + +_BASE_HEADERS = ["name", "y", "x", "positions_noise"] +_FLUX_HEADERS = ["flux", "flux_noise"] +_TIME_DELAY_HEADERS = ["time_delay", "time_delay_noise"] +_REDSHIFT_HEADERS = ["redshift"] + + +class PointDataset: + def __init__( + self, + name: str, + positions: Union[aa.Grid2DIrregular, List[List], List[Tuple]], + positions_noise_map: Union[float, aa.ArrayIrregular, List[float]], + fluxes: Optional[Union[aa.ArrayIrregular, List[float]]] = None, + fluxes_noise_map: Optional[Union[float, aa.ArrayIrregular, List[float]]] = None, + time_delays: Optional[Union[aa.ArrayIrregular, List[float]]] = None, + time_delays_noise_map: Optional[ + Union[float, aa.ArrayIrregular, List[float]] + ] = None, + redshift: Optional[float] = None, + ): + """ + A collection of the data component that can be used for point-source model-fitting, for example fitting the + observed positions of a a strongly lensed quasar or supernovae or in strong lens cluster modeling, where + there may be many tens or hundreds of individual source galaxies each of which are modeled as a point source. + + The name of the dataset is required for point-source model-fitting, as it pairs a point-source dataset with + its corresponding point-source in the model-fit. For example, if a dataset has the name `source_1`, it will + be paired with the `Point` model-component which has the name `source_1`. If a dataset component is not + successfully paired with a model-component, an error is raised. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` in the `Model`. + positions + The image-plane (y,x) positions of the point-source. + positions_noise_map + The noise-value of every (y,x) position, which is typically the pixel-scale of the data. + fluxes + The image-plane flux of each observed point-source of light. + fluxes_noise_map + The noise-value of every observed flux, which is typically measured from the pixel values of the pixel + containing the point source after convolution with the PSF. + time_delays + The time delays of each observed point-source of light in days. + time_delays_noise_map + The noise-value of every observed time delay, which is typically measured from the time delay analysis. + redshift + The redshift of the source. Optional; when provided it is carried through CSV round-trips alongside + the positions so cluster-scale workflows can encode per-source redshifts in a single spreadsheet. + """ + + self.name = name + + # Ensure positions is a Grid2DIrregular + self.positions = ( + positions + if isinstance(positions, aa.Grid2DIrregular) + else aa.Grid2DIrregular(values=positions) + ) + + # Ensure positions_noise_map is an ArrayIrregular + if isinstance(positions_noise_map, float): + positions_noise_map = [positions_noise_map] * len(self.positions) + + self.positions_noise_map = ( + positions_noise_map + if isinstance(positions_noise_map, aa.ArrayIrregular) + else aa.ArrayIrregular(values=positions_noise_map) + ) + + def convert_to_array_irregular(values): + """ + Convert data to ArrayIrregular if it is not already. + """ + return ( + aa.ArrayIrregular(values=values) + if values is not None and not isinstance(values, aa.ArrayIrregular) + else values + ) + + # Convert fluxes, time delays and their noise maps to ArrayIrregular if provided as values and not already this type + + self.fluxes = convert_to_array_irregular(fluxes) + self.fluxes_noise_map = convert_to_array_irregular(fluxes_noise_map) + self.time_delays = convert_to_array_irregular(time_delays) + self.time_delays_noise_map = convert_to_array_irregular(time_delays_noise_map) + + self.redshift = float(redshift) if redshift is not None else None + + @property + def info(self) -> str: + """ + A dictionary representation of this instance. + + Arrays are represented as lists or lists of lists. + """ + info = f"name : {self.name}\n" + info += f"positions : {self.positions}\n" + info += f"positions_noise_map : {self.positions_noise_map}\n" + info += f"fluxes : {self.fluxes}\n" + info += f"fluxes_noise_map : {self.fluxes_noise_map}\n" + info += f"time_delays : {self.time_delays}\n" + info += f"time_delays_noise_map : {self.time_delays_noise_map}\n" + info += f"redshift : {self.redshift}\n" + return info + + def extent_from(self, buffer: float = 0.1): + y_max = max(self.positions[:, 0]) + buffer + y_min = min(self.positions[:, 0]) - buffer + x_max = max(self.positions[:, 1]) + buffer + x_min = min(self.positions[:, 1]) - buffer + + return [y_min, y_max, x_min, x_max] + + def to_csv(self, file_path: str): + """ + Write this dataset to ``file_path`` as a CSV with one row per image. + + Optional flux / time-delay columns are included only when this dataset carries + the corresponding values. For multi-dataset output use :func:`output_to_csv`. + """ + output_to_csv([self], file_path) + + @classmethod + def from_csv( + cls, file_path: str, name: Optional[str] = None + ) -> "PointDataset": + """ + Load a single ``PointDataset`` from a CSV written by :meth:`to_csv` or + :func:`output_to_csv`. + + Parameters + ---------- + file_path + Path to a CSV file with at minimum the columns + ``name, y, x, positions_noise``. + name + The ``name`` group to load. Must be provided when the CSV contains more + than one ``name``; when the CSV contains exactly one group it is picked + automatically. + """ + datasets = list_from_csv(file_path) + + if not datasets: + raise ValueError( + f"CSV file {file_path!r} contained no PointDataset rows." + ) + + if name is None: + if len(datasets) > 1: + available = [d.name for d in datasets] + raise ValueError( + f"CSV file {file_path!r} contains {len(datasets)} groups " + f"({available!r}); pass name= to select one." + ) + return datasets[0] + + for dataset in datasets: + if dataset.name == name: + return dataset + + available = [d.name for d in datasets] + raise ValueError( + f"CSV file {file_path!r} has no group named {name!r}. " + f"Available groups: {available!r}." + ) + + +def _optional_values(dataset: PointDataset, attr: str) -> Optional[List[float]]: + values = getattr(dataset, attr) + if values is None: + return None + return [float(v) for v in values] + + +def output_to_csv(datasets: List[PointDataset], file_path: str): + """ + Write a list of ``PointDataset`` objects to a single CSV with one row per observed + image. + + The base columns (``name, y, x, positions_noise``) are always written. The + optional ``flux``/``flux_noise``, ``time_delay``/``time_delay_noise`` and + ``redshift`` columns are included when *any* dataset in ``datasets`` carries + those values; datasets that do not carry them leave those cells blank. + + When written, every row in a given ``name`` group repeats the same ``redshift`` + value — the source redshift is a per-source property, not per-image. + + This is the hand-editable / spreadsheet form preferred for strong-lens cluster + workflows with tens or hundreds of multiply-imaged sources. For exact + round-trip serialisation use ``output_to_json`` / ``from_json``. + """ + include_flux = any(d.fluxes is not None for d in datasets) + include_time_delay = any(d.time_delays is not None for d in datasets) + include_redshift = any(d.redshift is not None for d in datasets) + + headers = list(_BASE_HEADERS) + if include_flux: + headers += _FLUX_HEADERS + if include_time_delay: + headers += _TIME_DELAY_HEADERS + if include_redshift: + headers += _REDSHIFT_HEADERS + + rows = [] + for dataset in datasets: + positions = dataset.positions + positions_noise = _optional_values(dataset, "positions_noise_map") + fluxes = _optional_values(dataset, "fluxes") + fluxes_noise = _optional_values(dataset, "fluxes_noise_map") + time_delays = _optional_values(dataset, "time_delays") + time_delays_noise = _optional_values(dataset, "time_delays_noise_map") + + for i in range(len(positions)): + row = { + "name": dataset.name, + "y": float(positions[i][0]), + "x": float(positions[i][1]), + "positions_noise": positions_noise[i], + } + if include_flux: + row["flux"] = "" if fluxes is None else fluxes[i] + row["flux_noise"] = ( + "" if fluxes_noise is None else fluxes_noise[i] + ) + if include_time_delay: + row["time_delay"] = ( + "" if time_delays is None else time_delays[i] + ) + row["time_delay_noise"] = ( + "" if time_delays_noise is None else time_delays_noise[i] + ) + if include_redshift: + row["redshift"] = ( + "" if dataset.redshift is None else dataset.redshift + ) + rows.append(row) + + csvable.output_to_csv(rows, file_path, headers=headers) + + +def _float_column( + group_rows: List[dict], column: str, group_name: str +) -> Optional[List[float]]: + raw = [row.get(column, "") for row in group_rows] + populated = [v for v in raw if v not in ("", None)] + + if not populated: + return None + + if len(populated) != len(raw): + raise ValueError( + f"CSV group {group_name!r} has partially populated column " + f"{column!r}; every row in the group must have a value or all be blank." + ) + + return [float(v) for v in raw] + + +def _group_redshift( + group_rows: List[dict], group_name: str +) -> Optional[float]: + raw = [row.get("redshift", "") for row in group_rows] + populated = [v for v in raw if v not in ("", None)] + + if not populated: + return None + + if len(populated) != len(raw): + raise ValueError( + f"CSV group {group_name!r} has partially populated column " + f"'redshift'; every row in the group must have a value or all be blank." + ) + + values = [float(v) for v in populated] + if any(v != values[0] for v in values): + raise ValueError( + f"CSV group {group_name!r} has inconsistent 'redshift' values " + f"{values!r}; a source redshift must be identical across all of its " + f"image rows." + ) + + return values[0] + + +def list_from_csv(file_path: str) -> List[PointDataset]: + """ + Load a list of ``PointDataset`` objects from a CSV written by + :func:`output_to_csv` (or :meth:`PointDataset.to_csv`). + + Rows are grouped by their ``name`` column — one ``PointDataset`` per distinct + name, preserving the order of first appearance. Optional per-image columns + (``flux``/``flux_noise``, ``time_delay``/``time_delay_noise``) are carried through + per-group: if every row in a group populates the column the values are loaded, + if every row leaves it blank the corresponding attribute is set to ``None``, and + any partial-population is rejected with a ``ValueError``. + + The optional ``redshift`` column is per-source (not per-image): every row within + a group must share the same value. A group with mixed or differing redshifts is + rejected with a ``ValueError``. + """ + rows = csvable.list_from_csv(file_path) + + if not rows: + return [] + + headers = list(rows[0].keys()) + + for required in _BASE_HEADERS: + if required not in headers: + raise ValueError( + f"CSV file {file_path!r} is missing required column {required!r}; " + f"expected headers starting with {_BASE_HEADERS!r}." + ) + + groups: "dict[str, List[dict]]" = {} + for row in rows: + groups.setdefault(row["name"], []).append(row) + + has_flux_column = "flux" in headers + has_flux_noise_column = "flux_noise" in headers + has_time_delay_column = "time_delay" in headers + has_time_delay_noise_column = "time_delay_noise" in headers + has_redshift_column = "redshift" in headers + + datasets: List[PointDataset] = [] + for name, group_rows in groups.items(): + positions = [(float(r["y"]), float(r["x"])) for r in group_rows] + positions_noise_map = [ + float(r["positions_noise"]) for r in group_rows + ] + + fluxes = ( + _float_column(group_rows, "flux", name) + if has_flux_column + else None + ) + fluxes_noise_map = ( + _float_column(group_rows, "flux_noise", name) + if has_flux_noise_column + else None + ) + time_delays = ( + _float_column(group_rows, "time_delay", name) + if has_time_delay_column + else None + ) + time_delays_noise_map = ( + _float_column(group_rows, "time_delay_noise", name) + if has_time_delay_noise_column + else None + ) + redshift = ( + _group_redshift(group_rows, name) + if has_redshift_column + else None + ) + + datasets.append( + PointDataset( + name=name, + positions=positions, + positions_noise_map=positions_noise_map, + fluxes=fluxes, + fluxes_noise_map=fluxes_noise_map, + time_delays=time_delays, + time_delays_noise_map=time_delays_noise_map, + redshift=redshift, + ) + ) + + return datasets diff --git a/autolens/point/fit/abstract.py b/autolens/point/fit/abstract.py index 281d197f8..b6559a579 100644 --- a/autolens/point/fit/abstract.py +++ b/autolens/point/fit/abstract.py @@ -1,184 +1,184 @@ -""" -Abstract base class for all point-source fit components. - -``AbstractFitPoint`` provides shared functionality that is common to all point-source fit -classes (position fits, flux fits, time-delay fits): - -- Solving for the image-plane positions of a source-plane coordinate using a - ``PointSolver``. -- Computing the magnification at each image position from the tracer's mass model. -- Computing deflection angles at each image position. -- Providing the name-based pairing between a ``PointDataset`` and its ``Point`` model - component. - -Concrete subclasses implement ``figure_of_merit`` to compute the chi-squared or -log-likelihood for the specific observable (positions, fluxes, time delays) being fitted. -""" -from abc import ABC -import numpy as np -from typing import Optional, Tuple - -import autoarray as aa -import autogalaxy as ag - -from autolens.point.solver import PointSolver -from autolens.lens.tracer import Tracer - -from autolens import exc - - -class AbstractFitPoint(aa.AbstractFit, ABC): - def __init__( - self, - name: str, - data: aa.Grid2DIrregular, - noise_map: aa.ArrayIrregular, - tracer: Tracer, - solver: PointSolver, - profile: Optional[ag.ps.Point] = None, - xp=np, - ): - """ - Abstract class to fit a point source dataset using a `Tracer` object, including different components - of the point source data like positions, fluxes and time delays. - - All sub-classes which fit specifc components of the point source data (e.g. positions, fluxes, time delays) - inherit from this class, to provide them with general point-source functionality used in most - calculations, for example the deflection angles and magnification of the point source. - - Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the - point source dataset to ensure that point source datasets are fitted to the correct point source. - - This fit object is used in the `FitPointDataset` to perform fitting of a `PointDataset`, which may also fit - the point dataset poisitions, fluxes and / or time delays. - - When performing a model-fit via an `AnalysisPoint` object the `figure_of_merit` of the child class - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` profile. - data - The positions of the point source in the image-plane which are fitted. - noise_map - The noise-map of the positions which are used to compute the log likelihood of the positions. - tracer - The tracer of galaxies whose point source profile are used to fit the positions. - solver - Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing - triangles to and from the source-plane. - profile - Manually input the profile of the point source, which is used instead of the one extracted from the - tracer via name pairing if that profile is not found. - """ - - self.name = name - if xp is not np and data._xp is not xp: - data = aa.Grid2DIrregular(values=data.array, xp=xp) - self._data = data - self._noise_map = noise_map - self.tracer = tracer - self.solver = solver - - self.profile = profile or tracer.extract_profile(profile_name=name) - - if self.profile is None: - raise exc.PointExtractionException( - f"For the point-source named {name} there was no matching point source profile " - f"in the tracer (make sure your tracer's point source name is the same the dataset name." - ) - - self.use_jax = xp is not np - - @property - def _xp(self): - if self.use_jax: - import jax.numpy as jnp - - return jnp - return np - - @property - def data(self): - return self._data - - @property - def noise_map(self): - return self._noise_map - - @property - def magnifications_at_positions(self) -> aa.ArrayIrregular: - """ - The magnification of every observed position in the image-plane, which is computed from the tracer's deflection - angle map via the Hessian. - - These magnifications are used for two purposes: - - 1) For a source-plane chi-squared calculation, the residuals are multiplied by the magnification to account for - how the noise in the image-plane positions is magnified to the source-plane, thus defining a - better chi-squared. - - 2) For fitting the fluxes of point sources, the magnification is used to scale the flux of the point source - in the source-plane to the image-plane, thus computing the model image-plane fluxes. - """ - use_multi_plane = len(self.tracer.planes) > 2 - plane_j = ( - self.tracer.extract_plane_index_of_profile(profile_name=self.name) - if use_multi_plane - else -1 - ) - od = ag.LensCalc.from_tracer( - tracer=self.tracer, - use_multi_plane=use_multi_plane, - plane_j=plane_j, - ) - magnifications = od.magnification_2d_via_hessian_from( - grid=self.positions, xp=self._xp - ) - if self.use_jax: - magnifications = aa.ArrayIrregular(values=magnifications) - return abs(magnifications) - - @property - def source_plane_coordinate(self) -> Tuple[float, float]: - """ - Returns the centre of the point-source in the source-plane, which is used when computing the model - image-plane positions from the tracer. - - Returns - ------- - The (y,x) arc-second coordinates of the point-source in the source-plane. - """ - return self.profile.centre - - @property - def plane_index(self) -> int: - """ - Returns the integer plane index containing the point source galaxy, which is used when computing the deflection - angles of image-plane positions from the tracer. - - This index is used to ensure that if multi-plane tracing is used when solving the model image-plane positions, - the correct source-plane is used to compute the model positions whilst accounting for multi-plane lensing. - - Returns - ------- - The index of the plane containing the point-source galaxy. - """ - return self.tracer.extract_plane_index_of_profile(profile_name=self.name) - - @property - def plane_redshift(self) -> float: - """ - Returns the redshift of the plane containing the point source galaxy, which is used when computing the - deflection angles of image-plane positions from the tracer. - - This redshift is used to ensure that if multi-plane tracing is used when solving the model image-plane - positions, the correct source-plane is used to compute the model positions whilst accounting for multi-plane - lensing. - - Returns - ------- - The redshift of the plane containing the point-source galaxy. - """ - return self.tracer.planes[self.plane_index].redshift +""" +Abstract base class for all point-source fit components. + +``AbstractFitPoint`` provides shared functionality that is common to all point-source fit +classes (position fits, flux fits, time-delay fits): + +- Solving for the image-plane positions of a source-plane coordinate using a + ``PointSolver``. +- Computing the magnification at each image position from the tracer's mass model. +- Computing deflection angles at each image position. +- Providing the name-based pairing between a ``PointDataset`` and its ``Point`` model + component. + +Concrete subclasses implement ``figure_of_merit`` to compute the chi-squared or +log-likelihood for the specific observable (positions, fluxes, time delays) being fitted. +""" +from abc import ABC +import numpy as np +from typing import Optional, Tuple + +import autoarray as aa +import autogalaxy as ag + +from autolens.point.solver import PointSolver +from autolens.lens.tracer import Tracer + +from autolens import exc + + +class AbstractFitPoint(aa.AbstractFit, ABC): + def __init__( + self, + name: str, + data: aa.Grid2DIrregular, + noise_map: aa.ArrayIrregular, + tracer: Tracer, + solver: PointSolver, + profile: Optional[ag.ps.Point] = None, + xp=np, + ): + """ + Abstract class to fit a point source dataset using a `Tracer` object, including different components + of the point source data like positions, fluxes and time delays. + + All sub-classes which fit specifc components of the point source data (e.g. positions, fluxes, time delays) + inherit from this class, to provide them with general point-source functionality used in most + calculations, for example the deflection angles and magnification of the point source. + + Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the + point source dataset to ensure that point source datasets are fitted to the correct point source. + + This fit object is used in the `FitPointDataset` to perform fitting of a `PointDataset`, which may also fit + the point dataset poisitions, fluxes and / or time delays. + + When performing a model-fit via an `AnalysisPoint` object the `figure_of_merit` of the child class + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` profile. + data + The positions of the point source in the image-plane which are fitted. + noise_map + The noise-map of the positions which are used to compute the log likelihood of the positions. + tracer + The tracer of galaxies whose point source profile are used to fit the positions. + solver + Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing + triangles to and from the source-plane. + profile + Manually input the profile of the point source, which is used instead of the one extracted from the + tracer via name pairing if that profile is not found. + """ + + self.name = name + if xp is not np and data._xp is not xp: + data = aa.Grid2DIrregular(values=data.array, xp=xp) + self._data = data + self._noise_map = noise_map + self.tracer = tracer + self.solver = solver + + self.profile = profile or tracer.extract_profile(profile_name=name) + + if self.profile is None: + raise exc.PointExtractionException( + f"For the point-source named {name} there was no matching point source profile " + f"in the tracer (make sure your tracer's point source name is the same the dataset name." + ) + + self.use_jax = xp is not np + + @property + def _xp(self): + if self.use_jax: + import jax.numpy as jnp + + return jnp + return np + + @property + def data(self): + return self._data + + @property + def noise_map(self): + return self._noise_map + + @property + def magnifications_at_positions(self) -> aa.ArrayIrregular: + """ + The magnification of every observed position in the image-plane, which is computed from the tracer's deflection + angle map via the Hessian. + + These magnifications are used for two purposes: + + 1) For a source-plane chi-squared calculation, the residuals are multiplied by the magnification to account for + how the noise in the image-plane positions is magnified to the source-plane, thus defining a + better chi-squared. + + 2) For fitting the fluxes of point sources, the magnification is used to scale the flux of the point source + in the source-plane to the image-plane, thus computing the model image-plane fluxes. + """ + use_multi_plane = len(self.tracer.planes) > 2 + plane_j = ( + self.tracer.extract_plane_index_of_profile(profile_name=self.name) + if use_multi_plane + else -1 + ) + od = ag.LensCalc.from_tracer( + tracer=self.tracer, + use_multi_plane=use_multi_plane, + plane_j=plane_j, + ) + magnifications = od.magnification_2d_via_hessian_from( + grid=self.positions, xp=self._xp + ) + if self.use_jax: + magnifications = aa.ArrayIrregular(values=magnifications) + return abs(magnifications) + + @property + def source_plane_coordinate(self) -> Tuple[float, float]: + """ + Returns the centre of the point-source in the source-plane, which is used when computing the model + image-plane positions from the tracer. + + Returns + ------- + The (y,x) arc-second coordinates of the point-source in the source-plane. + """ + return self.profile.centre + + @property + def plane_index(self) -> int: + """ + Returns the integer plane index containing the point source galaxy, which is used when computing the deflection + angles of image-plane positions from the tracer. + + This index is used to ensure that if multi-plane tracing is used when solving the model image-plane positions, + the correct source-plane is used to compute the model positions whilst accounting for multi-plane lensing. + + Returns + ------- + The index of the plane containing the point-source galaxy. + """ + return self.tracer.extract_plane_index_of_profile(profile_name=self.name) + + @property + def plane_redshift(self) -> float: + """ + Returns the redshift of the plane containing the point source galaxy, which is used when computing the + deflection angles of image-plane positions from the tracer. + + This redshift is used to ensure that if multi-plane tracing is used when solving the model image-plane + positions, the correct source-plane is used to compute the model positions whilst accounting for multi-plane + lensing. + + Returns + ------- + The redshift of the plane containing the point-source galaxy. + """ + return self.tracer.planes[self.plane_index].redshift diff --git a/autolens/point/fit/dataset.py b/autolens/point/fit/dataset.py index b86f43169..8d2df4c77 100644 --- a/autolens/point/fit/dataset.py +++ b/autolens/point/fit/dataset.py @@ -1,181 +1,181 @@ -""" -Top-level fit class for a complete point-source dataset. - -``FitPointDataset`` orchestrates fitting of all observables in a ``PointDataset`` -(image-plane positions, fluxes, and/or time delays) simultaneously. It creates and -stores individual fit objects for each component that is present in the dataset: - -- ``FitPositionsImagePair`` (or another positions fit class) — fits image-plane positions. -- ``FitFluxes`` — fits flux ratios (if fluxes are in the dataset). -- ``FitTimeDelays`` — fits time delays (if time delays are in the dataset). - -The ``log_likelihood`` is the sum of the individual component log likelihoods. This -class is used by ``AnalysisPoint`` as the evaluation engine inside the -``log_likelihood_function``. -""" -import numpy as np - -from autolens.point.dataset import PointDataset -from autolens.point.solver import PointSolver -from autolens.point.fit.fluxes import FitFluxes -from autolens.point.fit.times_delays import FitTimeDelays -from autolens.lens.tracer import Tracer - -from autolens.point.fit.positions.image.pair import FitPositionsImagePair -from autolens import exc - - -class FitPointDataset: - def __init__( - self, - dataset: PointDataset, - tracer: Tracer, - solver: PointSolver, - fit_positions_cls=FitPositionsImagePair, - xp=np, - ): - """ - Fits a point source dataset using a `Tracer` object, where the following components of the point source data - may be fitted: - - - The positions of the point source in the image-plane, where the chi-squared could be defined as an image-plane - or source-plane chi-squared. - - - The fluxes of the point source, which use the magnification of the point source to compute the fluxes in the - image-plane. - - - The time delays of the point source in delays, which use the tracer to compute the model time delays - at the image-plane positions of the point source in the dataset. - - The fit may use one or combinations of the above components to compute the log likelihood, depending on what - components are available in the point source dataset and the model point source profiles input. For example: - - - The `ps.Point` object has a `centre` but does not have a flux, so the fluxes are not fitted, meaning only - positions are fitted. - - - The `ps.PointFlux` object has a `centre` and a flux, therefore both the positions and fluxes are fitted. - - The fit performs the following steps: - - 1) Fit the positions of the point source dataset using the input `fit_positions_cls` object, which could be an - image-plane or source-plane chi-squared. - - 2) Fit the fluxes of the point source dataset using the `FitFluxes` object, where the object type may be - extended in the future to support different types of point source profiles. - - 3) Fits the time delays of the point source dataset using the `FitTimeDelays` object, which is an image-plane - evaluation of the time delays at the image-plane positions of the point source in the dataset. - - Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the - point source dataset to ensure that point source datasets are fitted to the correct point source. - - When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - dataset - The point source dataset which is fitted. - tracer - The tracer of galaxies whose point source profile are used to fit the positions. - solver - Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing - triangles to and from the source-plane. - fit_positions_cls - The class used to fit the positions of the point source dataset, which could be an image-plane or - source-plane chi-squared. - profile - Manually input the profile of the point source, which is used instead of the one extracted from the - tracer via name pairing if that profile is not found. - """ - self.dataset = dataset - self.tracer = tracer - self.solver = solver - - profile = self.tracer.extract_profile(profile_name=dataset.name) - - self.fit_positions_cls = fit_positions_cls - - try: - self.positions = self.fit_positions_cls( - name=dataset.name, - data=dataset.positions, - noise_map=dataset.positions_noise_map, - tracer=tracer, - solver=solver, - profile=profile, - xp=xp, - ) - except exc.PointExtractionException: - self.positions = None - - try: - if dataset.fluxes is not None: - self.flux = FitFluxes( - name=dataset.name, - data=dataset.fluxes, - noise_map=dataset.fluxes_noise_map, - positions=dataset.positions, - tracer=tracer, - xp=xp, - ) - else: - self.flux = None - - except exc.PointExtractionException: - self.flux = None - - try: - if dataset.time_delays is not None: - self.time_delays = FitTimeDelays( - name=dataset.name, - data=dataset.time_delays, - noise_map=dataset.time_delays_noise_map, - positions=dataset.positions, - tracer=tracer, - xp=xp, - ) - else: - self.time_delays = None - except exc.PointExtractionException: - self.time_delays = None - - self.use_jax = xp is not np - - @property - def _xp(self): - if self.use_jax: - import jax.numpy as jnp - - return jnp - return np - - @property - def model_obj(self): - return self.tracer - - @property - def log_likelihood(self) -> float: - """ - Returns the overall `log_likelihood` of the point source dataset, which is the sum of the log likelihoods of - each individual component of the point source dataset that is fitted (e.g. positions, fluxes, time delays). - """ - log_likelihood_positions = ( - self.positions.log_likelihood if self.positions is not None else 0.0 - ) - log_likelihood_flux = self.flux.log_likelihood if self.flux is not None else 0.0 - log_likelihood_time_delays = ( - self.time_delays.log_likelihood if self.time_delays is not None else 0.0 - ) - - return ( - log_likelihood_positions + log_likelihood_flux + log_likelihood_time_delays - ) - - @property - def figure_of_merit(self) -> float: - """ - The `figure_of_merit` of the point source dataset, which is the value the `AnalysisPoint` object calls to - perform a model-fit. - """ - return self.log_likelihood +""" +Top-level fit class for a complete point-source dataset. + +``FitPointDataset`` orchestrates fitting of all observables in a ``PointDataset`` +(image-plane positions, fluxes, and/or time delays) simultaneously. It creates and +stores individual fit objects for each component that is present in the dataset: + +- ``FitPositionsImagePair`` (or another positions fit class) — fits image-plane positions. +- ``FitFluxes`` — fits flux ratios (if fluxes are in the dataset). +- ``FitTimeDelays`` — fits time delays (if time delays are in the dataset). + +The ``log_likelihood`` is the sum of the individual component log likelihoods. This +class is used by ``AnalysisPoint`` as the evaluation engine inside the +``log_likelihood_function``. +""" +import numpy as np + +from autolens.point.dataset import PointDataset +from autolens.point.solver import PointSolver +from autolens.point.fit.fluxes import FitFluxes +from autolens.point.fit.times_delays import FitTimeDelays +from autolens.lens.tracer import Tracer + +from autolens.point.fit.positions.image.pair import FitPositionsImagePair +from autolens import exc + + +class FitPointDataset: + def __init__( + self, + dataset: PointDataset, + tracer: Tracer, + solver: PointSolver, + fit_positions_cls=FitPositionsImagePair, + xp=np, + ): + """ + Fits a point source dataset using a `Tracer` object, where the following components of the point source data + may be fitted: + + - The positions of the point source in the image-plane, where the chi-squared could be defined as an image-plane + or source-plane chi-squared. + + - The fluxes of the point source, which use the magnification of the point source to compute the fluxes in the + image-plane. + + - The time delays of the point source in delays, which use the tracer to compute the model time delays + at the image-plane positions of the point source in the dataset. + + The fit may use one or combinations of the above components to compute the log likelihood, depending on what + components are available in the point source dataset and the model point source profiles input. For example: + + - The `ps.Point` object has a `centre` but does not have a flux, so the fluxes are not fitted, meaning only + positions are fitted. + + - The `ps.PointFlux` object has a `centre` and a flux, therefore both the positions and fluxes are fitted. + + The fit performs the following steps: + + 1) Fit the positions of the point source dataset using the input `fit_positions_cls` object, which could be an + image-plane or source-plane chi-squared. + + 2) Fit the fluxes of the point source dataset using the `FitFluxes` object, where the object type may be + extended in the future to support different types of point source profiles. + + 3) Fits the time delays of the point source dataset using the `FitTimeDelays` object, which is an image-plane + evaluation of the time delays at the image-plane positions of the point source in the dataset. + + Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the + point source dataset to ensure that point source datasets are fitted to the correct point source. + + When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + dataset + The point source dataset which is fitted. + tracer + The tracer of galaxies whose point source profile are used to fit the positions. + solver + Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing + triangles to and from the source-plane. + fit_positions_cls + The class used to fit the positions of the point source dataset, which could be an image-plane or + source-plane chi-squared. + profile + Manually input the profile of the point source, which is used instead of the one extracted from the + tracer via name pairing if that profile is not found. + """ + self.dataset = dataset + self.tracer = tracer + self.solver = solver + + profile = self.tracer.extract_profile(profile_name=dataset.name) + + self.fit_positions_cls = fit_positions_cls + + try: + self.positions = self.fit_positions_cls( + name=dataset.name, + data=dataset.positions, + noise_map=dataset.positions_noise_map, + tracer=tracer, + solver=solver, + profile=profile, + xp=xp, + ) + except exc.PointExtractionException: + self.positions = None + + try: + if dataset.fluxes is not None: + self.flux = FitFluxes( + name=dataset.name, + data=dataset.fluxes, + noise_map=dataset.fluxes_noise_map, + positions=dataset.positions, + tracer=tracer, + xp=xp, + ) + else: + self.flux = None + + except exc.PointExtractionException: + self.flux = None + + try: + if dataset.time_delays is not None: + self.time_delays = FitTimeDelays( + name=dataset.name, + data=dataset.time_delays, + noise_map=dataset.time_delays_noise_map, + positions=dataset.positions, + tracer=tracer, + xp=xp, + ) + else: + self.time_delays = None + except exc.PointExtractionException: + self.time_delays = None + + self.use_jax = xp is not np + + @property + def _xp(self): + if self.use_jax: + import jax.numpy as jnp + + return jnp + return np + + @property + def model_obj(self): + return self.tracer + + @property + def log_likelihood(self) -> float: + """ + Returns the overall `log_likelihood` of the point source dataset, which is the sum of the log likelihoods of + each individual component of the point source dataset that is fitted (e.g. positions, fluxes, time delays). + """ + log_likelihood_positions = ( + self.positions.log_likelihood if self.positions is not None else 0.0 + ) + log_likelihood_flux = self.flux.log_likelihood if self.flux is not None else 0.0 + log_likelihood_time_delays = ( + self.time_delays.log_likelihood if self.time_delays is not None else 0.0 + ) + + return ( + log_likelihood_positions + log_likelihood_flux + log_likelihood_time_delays + ) + + @property + def figure_of_merit(self) -> float: + """ + The `figure_of_merit` of the point source dataset, which is the value the `AnalysisPoint` object calls to + perform a model-fit. + """ + return self.log_likelihood diff --git a/autolens/point/fit/fluxes.py b/autolens/point/fit/fluxes.py index 70da16345..01899611c 100644 --- a/autolens/point/fit/fluxes.py +++ b/autolens/point/fit/fluxes.py @@ -1,148 +1,148 @@ -""" -Flux-ratio fit component for point-source lensing. - -``FitFluxes`` computes the likelihood of the observed image-plane fluxes given the -predicted magnification ratios from the tracer's mass model. - -The predicted fluxes are proportional to the absolute magnification at each solved image -position, normalised so that the brightest image has flux 1.0 (flux ratios). A chi-squared -is computed against the observed flux values and noise map, contributing to the total -``FitPointDataset`` log likelihood. -""" -import numpy as np -from typing import Optional - -import autoarray as aa -import autogalaxy as ag - -from autolens.point.fit.abstract import AbstractFitPoint -from autolens.lens.tracer import Tracer - -from autolens import exc - - -class FitFluxes(AbstractFitPoint): - def __init__( - self, - name: str, - data: aa.ArrayIrregular, - noise_map: aa.ArrayIrregular, - positions: aa.Grid2DIrregular, - tracer: Tracer, - profile: Optional[ag.ps.Point] = None, - xp=np, - ): - """ - Fits the fluxes of a a point source dataset using a `Tracer` object, where every model flux of the point-source - is compared with its observed flux. - - The fit performs the following steps: - - 1) Compute the magnification of the input image-plane `positions` via the Hessian of the tracer's deflection angles. - - 2) Determine the image-plane model fluxes by multiplying the source-plane flux with these magnifications. - - 3) Subtract the observed fluxes from the model fluxes to compute the residual fluxes, called the `residual_map`. - - 4) Compute the chi-squared of each flux as the square of the residual divided by the RMS noise-map value. - - 5) Sum the chi-squared values to compute the overall log likelihood of the fit. - - Flux based fitting in the source code always inputs the observed positions of the point dataset as the input - `positions`, but the following changes could be implemented and used in the future: - - - Use the model positions instead of the observed positions to compute the fluxes, which would therefore - require the centre of the point source in the source-plane to be used and for the `PointSolver` to determine - the image-plane positions via ray-tracing triangles to and from the source-plane. This would require - care in pairing model positions to observed positions where fluxes are computed. - - - The "size" of the point-source is not currently supported, however the `ShapeSolver` implemented in the - source code does allow for magnifications to be computed based on point sources with a shape (e.g. a - `Circle` where its radius is a free parameter). - - Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the - point source dataset to ensure that point source datasets are fitted to the correct point source. - - This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, - which may also fit other components of the point dataset like fluxes or time delays. - - When performing a `model-fit` via an `AnalysisPoint` object the `figure_of_merit` of this object - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` profile. - data - The positions of the point source in the image-plane which are fitted. - noise_map - The noise-map of the positions which are used to compute the log likelihood of the positions. - tracer - The tracer of galaxies whose point source profile are used to fit the positions. - positions - The positions of the point source in the image-plane where the fluxes are calculated. These are currently - always the observed positions of the point source in the source code, but other positions, like the - model positions, could be used in the future. - profile - Manually input the profile of the point source, which is used instead of the one extracted from the - tracer via name pairing if that profile is not found. - """ - self.positions = positions - - super().__init__( - name=name, - data=data, - noise_map=noise_map, - tracer=tracer, - solver=None, - profile=profile, - xp=xp, - ) - - if not hasattr(self.profile, "flux"): - raise exc.PointExtractionException( - f"For the point-source named {name} the extracted point source was the " - f"class {self.profile.__class__.__name__} and therefore does " - f"not contain a flux component." - ) - - @property - def model_data(self): - """ - The model-fluxes of the tracer at each of the input image-plane positions. - - Only point sources which are a `PointFlux` type, and therefore which include a model parameter for its flux, - are used. - """ - return aa.ArrayIrregular( - values=self._xp.array( - [ - magnification * self.profile.flux - for magnification in self.magnifications_at_positions - ] - ) - ) - - @property - def model_fluxes(self) -> aa.ArrayIrregular: - return self.model_data - - @property - def residual_map(self) -> aa.ArrayIrregular: - """ - Returns the difference between the observed and model fluxes of the point source, which is the residual flux - of a point source flux fit. - """ - residual_map = super().residual_map - - return aa.ArrayIrregular(values=residual_map) - - @property - def chi_squared(self) -> float: - """ - Returns the chi-squared of the fit of the point source fluxes, which is the residual flux values divided by the - RMS noise-map values squared. - """ - return ag.util.fit.chi_squared_from( - chi_squared_map=self.chi_squared_map.array, - ) +""" +Flux-ratio fit component for point-source lensing. + +``FitFluxes`` computes the likelihood of the observed image-plane fluxes given the +predicted magnification ratios from the tracer's mass model. + +The predicted fluxes are proportional to the absolute magnification at each solved image +position, normalised so that the brightest image has flux 1.0 (flux ratios). A chi-squared +is computed against the observed flux values and noise map, contributing to the total +``FitPointDataset`` log likelihood. +""" +import numpy as np +from typing import Optional + +import autoarray as aa +import autogalaxy as ag + +from autolens.point.fit.abstract import AbstractFitPoint +from autolens.lens.tracer import Tracer + +from autolens import exc + + +class FitFluxes(AbstractFitPoint): + def __init__( + self, + name: str, + data: aa.ArrayIrregular, + noise_map: aa.ArrayIrregular, + positions: aa.Grid2DIrregular, + tracer: Tracer, + profile: Optional[ag.ps.Point] = None, + xp=np, + ): + """ + Fits the fluxes of a a point source dataset using a `Tracer` object, where every model flux of the point-source + is compared with its observed flux. + + The fit performs the following steps: + + 1) Compute the magnification of the input image-plane `positions` via the Hessian of the tracer's deflection angles. + + 2) Determine the image-plane model fluxes by multiplying the source-plane flux with these magnifications. + + 3) Subtract the observed fluxes from the model fluxes to compute the residual fluxes, called the `residual_map`. + + 4) Compute the chi-squared of each flux as the square of the residual divided by the RMS noise-map value. + + 5) Sum the chi-squared values to compute the overall log likelihood of the fit. + + Flux based fitting in the source code always inputs the observed positions of the point dataset as the input + `positions`, but the following changes could be implemented and used in the future: + + - Use the model positions instead of the observed positions to compute the fluxes, which would therefore + require the centre of the point source in the source-plane to be used and for the `PointSolver` to determine + the image-plane positions via ray-tracing triangles to and from the source-plane. This would require + care in pairing model positions to observed positions where fluxes are computed. + + - The "size" of the point-source is not currently supported, however the `ShapeSolver` implemented in the + source code does allow for magnifications to be computed based on point sources with a shape (e.g. a + `Circle` where its radius is a free parameter). + + Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the + point source dataset to ensure that point source datasets are fitted to the correct point source. + + This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, + which may also fit other components of the point dataset like fluxes or time delays. + + When performing a `model-fit` via an `AnalysisPoint` object the `figure_of_merit` of this object + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` profile. + data + The positions of the point source in the image-plane which are fitted. + noise_map + The noise-map of the positions which are used to compute the log likelihood of the positions. + tracer + The tracer of galaxies whose point source profile are used to fit the positions. + positions + The positions of the point source in the image-plane where the fluxes are calculated. These are currently + always the observed positions of the point source in the source code, but other positions, like the + model positions, could be used in the future. + profile + Manually input the profile of the point source, which is used instead of the one extracted from the + tracer via name pairing if that profile is not found. + """ + self.positions = positions + + super().__init__( + name=name, + data=data, + noise_map=noise_map, + tracer=tracer, + solver=None, + profile=profile, + xp=xp, + ) + + if not hasattr(self.profile, "flux"): + raise exc.PointExtractionException( + f"For the point-source named {name} the extracted point source was the " + f"class {self.profile.__class__.__name__} and therefore does " + f"not contain a flux component." + ) + + @property + def model_data(self): + """ + The model-fluxes of the tracer at each of the input image-plane positions. + + Only point sources which are a `PointFlux` type, and therefore which include a model parameter for its flux, + are used. + """ + return aa.ArrayIrregular( + values=self._xp.array( + [ + magnification * self.profile.flux + for magnification in self.magnifications_at_positions + ] + ) + ) + + @property + def model_fluxes(self) -> aa.ArrayIrregular: + return self.model_data + + @property + def residual_map(self) -> aa.ArrayIrregular: + """ + Returns the difference between the observed and model fluxes of the point source, which is the residual flux + of a point source flux fit. + """ + residual_map = super().residual_map + + return aa.ArrayIrregular(values=residual_map) + + @property + def chi_squared(self) -> float: + """ + Returns the chi-squared of the fit of the point source fluxes, which is the residual flux values divided by the + RMS noise-map values squared. + """ + return ag.util.fit.chi_squared_from( + chi_squared_map=self.chi_squared_map.array, + ) diff --git a/autolens/point/fit/positions/abstract.py b/autolens/point/fit/positions/abstract.py index 77002fefc..1956e5ea3 100644 --- a/autolens/point/fit/positions/abstract.py +++ b/autolens/point/fit/positions/abstract.py @@ -1,92 +1,92 @@ -""" -Abstract base class for position-based point-source fit components. - -``AbstractFitPositions`` extends ``AbstractFitPoint`` with shared logic for all position -fitting strategies (image-plane pair fits and source-plane separation fits). It provides -common attributes — observed positions, solved image positions, deflection angles — and -leaves ``figure_of_merit`` to be implemented by each concrete subclass. - -The concrete subclasses (in the ``image/`` and ``source/`` sub-packages) implement -different statistical approaches for fitting image positions: - -- **Image-plane pair fits** — compare each observed position to the nearest predicted - image position, computing a chi-squared from the separation in arcseconds. -- **Source-plane separation fits** — trace each observed image back to the source plane - and compute the scatter of the traced positions around their mean, penalising poor - source-plane convergence. -""" -from abc import ABC -import numpy as np -from typing import Optional - -import autoarray as aa -import autogalaxy as ag - -from autolens.point.fit.abstract import AbstractFitPoint -from autolens.point.solver import PointSolver -from autolens.lens.tracer import Tracer - - -class AbstractFitPositions(AbstractFitPoint, ABC): - def __init__( - self, - name: str, - data: aa.Grid2DIrregular, - noise_map: aa.ArrayIrregular, - tracer: Tracer, - solver: PointSolver, - profile: Optional[ag.ps.Point] = None, - xp=np, - ): - """ - Abstract class to fit the positions of a a point source dataset using a `Tracer` object, where the specific - implementation of the chi-squared is defined in the sub-class. - - The fit performs the following steps: - - 1) Determine the source-plane centre of the point source, which could be a free model parameter or computed - as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). - - 2) Using the sub-class specific chi-squared, compute the residuals of each image-plane position, chi-squared - and overall log likelihood of the fit. - - Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the - point source dataset to ensure that point source datasets are fitted to the correct point source. - - This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, - which may also fit other components of the point dataset like fluxes or time delays. - - When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` profile. - data - The positions of the point source in the image-plane which are fitted. - noise_map - The noise-map of the positions which are used to compute the log likelihood of the positions. - tracer - The tracer of galaxies whose point source profile are used to fit the positions. - solver - Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing - triangles to and from the source-plane. - profile - Manually input the profile of the point source, which is used instead of the one extracted from the - tracer via name pairing if that profile is not found. - """ - - super().__init__( - name=name, - data=data, - noise_map=noise_map, - tracer=tracer, - solver=solver, - profile=profile, - xp=xp, - ) - - @property - def positions(self): - return self.data +""" +Abstract base class for position-based point-source fit components. + +``AbstractFitPositions`` extends ``AbstractFitPoint`` with shared logic for all position +fitting strategies (image-plane pair fits and source-plane separation fits). It provides +common attributes — observed positions, solved image positions, deflection angles — and +leaves ``figure_of_merit`` to be implemented by each concrete subclass. + +The concrete subclasses (in the ``image/`` and ``source/`` sub-packages) implement +different statistical approaches for fitting image positions: + +- **Image-plane pair fits** — compare each observed position to the nearest predicted + image position, computing a chi-squared from the separation in arcseconds. +- **Source-plane separation fits** — trace each observed image back to the source plane + and compute the scatter of the traced positions around their mean, penalising poor + source-plane convergence. +""" +from abc import ABC +import numpy as np +from typing import Optional + +import autoarray as aa +import autogalaxy as ag + +from autolens.point.fit.abstract import AbstractFitPoint +from autolens.point.solver import PointSolver +from autolens.lens.tracer import Tracer + + +class AbstractFitPositions(AbstractFitPoint, ABC): + def __init__( + self, + name: str, + data: aa.Grid2DIrregular, + noise_map: aa.ArrayIrregular, + tracer: Tracer, + solver: PointSolver, + profile: Optional[ag.ps.Point] = None, + xp=np, + ): + """ + Abstract class to fit the positions of a a point source dataset using a `Tracer` object, where the specific + implementation of the chi-squared is defined in the sub-class. + + The fit performs the following steps: + + 1) Determine the source-plane centre of the point source, which could be a free model parameter or computed + as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). + + 2) Using the sub-class specific chi-squared, compute the residuals of each image-plane position, chi-squared + and overall log likelihood of the fit. + + Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the + point source dataset to ensure that point source datasets are fitted to the correct point source. + + This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, + which may also fit other components of the point dataset like fluxes or time delays. + + When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` profile. + data + The positions of the point source in the image-plane which are fitted. + noise_map + The noise-map of the positions which are used to compute the log likelihood of the positions. + tracer + The tracer of galaxies whose point source profile are used to fit the positions. + solver + Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing + triangles to and from the source-plane. + profile + Manually input the profile of the point source, which is used instead of the one extracted from the + tracer via name pairing if that profile is not found. + """ + + super().__init__( + name=name, + data=data, + noise_map=noise_map, + tracer=tracer, + solver=solver, + profile=profile, + xp=xp, + ) + + @property + def positions(self): + return self.data diff --git a/autolens/point/fit/positions/image/abstract.py b/autolens/point/fit/positions/image/abstract.py index 5109d5efc..bd3e351eb 100644 --- a/autolens/point/fit/positions/image/abstract.py +++ b/autolens/point/fit/positions/image/abstract.py @@ -1,127 +1,127 @@ -""" -Abstract base for image-plane position fitting strategies. - -``AbstractFitPositionsImagePair`` solves for the predicted image positions using a -``PointSolver`` and then pairs each observed position with the closest predicted image, -computing a chi-squared from their separation. - -Three concrete pairing strategies are provided in this sub-package: - -- ``FitPositionsImagePair`` — pairs each observed to its nearest predicted image via - the Hungarian (linear sum assignment) algorithm. -- ``FitPositionsImagePairAll`` — computes the chi-squared for every observed/predicted - pair combination and takes the minimum. -- ``FitPositionsImagePairRepeat`` — allows predicted images to be paired to more than - one observed position, useful for highly magnified systems where some images may be - too close to separate. -""" - -from abc import ABC -import numpy as np -from typing import Optional - -import autoarray as aa -import autogalaxy as ag - -from autolens.point.solver import PointSolver -from autolens.point.fit.positions.abstract import AbstractFitPositions -from autolens.lens.tracer import Tracer - - -class AbstractFitPositionsImagePair(AbstractFitPositions, ABC): - def __init__( - self, - name: str, - data: aa.Grid2DIrregular, - noise_map: aa.ArrayIrregular, - tracer: Tracer, - solver: PointSolver, - profile: Optional[ag.ps.Point] = None, - xp=np, - ): - """ - Abstract class to fit the positions of a point source dataset using a `Tracer` object with an image-plane - chi-squared, where the specific implementation of the image-plane chi-squared is defined in the sub-class. - - The fit performs the following steps: - - 1) Determine the source-plane centre of the point source, which could be a free model parameter or computed - as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). - - 2) Determine the image-plane model positions using the `PointSolver` and the source-plane centre of the point - source (e.g. ray tracing triangles to and from the image and source planes), including accounting for - multi-plane ray-tracing. - - 3) Using the sub-class specific chi-squared, compute the residuals of each image-plane position, chi-squared - and overall log likelihood of the fit. - - Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the - point source dataset to ensure that point source datasets are fitted to the correct point source. - - This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, - which may also fit other components of the point dataset like fluxes or time delays. - - When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` profile. - data - The positions of the point source in the image-plane which are fitted. - noise_map - The noise-map of the positions which are used to compute the log likelihood of the positions. - tracer - The tracer of galaxies whose point source profile are used to fit the positions. - solver - Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing - triangles to and from the source-plane. - profile - Manually input the profile of the point source, which is used instead of the one extracted from the - tracer via name pairing if that profile is not found. - """ - - super().__init__( - name=name, - data=data, - noise_map=noise_map, - tracer=tracer, - solver=solver, - profile=profile, - xp=xp, - ) - - @staticmethod - def square_distance( - coord1: np.array, - coord2: np.array, - ) -> float: - """ - Calculate the square distance between two points. - - Parameters - ---------- - coord1 - The first point to calculate the distance between. - coord2 - The second point to calculate the distance between. - - Returns - ------- - The square distance between the two points - """ - return (coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2 - - @property - def model_data(self) -> aa.Grid2DIrregular: - """ - Returns the model positions, which are computed via the point solver. - """ - return self.solver.solve( - tracer=self.tracer, - source_plane_coordinate=self.source_plane_coordinate, - xp=self._xp, - plane_redshift=self.plane_redshift, - remove_infinities=False, - ) +""" +Abstract base for image-plane position fitting strategies. + +``AbstractFitPositionsImagePair`` solves for the predicted image positions using a +``PointSolver`` and then pairs each observed position with the closest predicted image, +computing a chi-squared from their separation. + +Three concrete pairing strategies are provided in this sub-package: + +- ``FitPositionsImagePair`` — pairs each observed to its nearest predicted image via + the Hungarian (linear sum assignment) algorithm. +- ``FitPositionsImagePairAll`` — computes the chi-squared for every observed/predicted + pair combination and takes the minimum. +- ``FitPositionsImagePairRepeat`` — allows predicted images to be paired to more than + one observed position, useful for highly magnified systems where some images may be + too close to separate. +""" + +from abc import ABC +import numpy as np +from typing import Optional + +import autoarray as aa +import autogalaxy as ag + +from autolens.point.solver import PointSolver +from autolens.point.fit.positions.abstract import AbstractFitPositions +from autolens.lens.tracer import Tracer + + +class AbstractFitPositionsImagePair(AbstractFitPositions, ABC): + def __init__( + self, + name: str, + data: aa.Grid2DIrregular, + noise_map: aa.ArrayIrregular, + tracer: Tracer, + solver: PointSolver, + profile: Optional[ag.ps.Point] = None, + xp=np, + ): + """ + Abstract class to fit the positions of a point source dataset using a `Tracer` object with an image-plane + chi-squared, where the specific implementation of the image-plane chi-squared is defined in the sub-class. + + The fit performs the following steps: + + 1) Determine the source-plane centre of the point source, which could be a free model parameter or computed + as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). + + 2) Determine the image-plane model positions using the `PointSolver` and the source-plane centre of the point + source (e.g. ray tracing triangles to and from the image and source planes), including accounting for + multi-plane ray-tracing. + + 3) Using the sub-class specific chi-squared, compute the residuals of each image-plane position, chi-squared + and overall log likelihood of the fit. + + Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the + point source dataset to ensure that point source datasets are fitted to the correct point source. + + This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, + which may also fit other components of the point dataset like fluxes or time delays. + + When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` profile. + data + The positions of the point source in the image-plane which are fitted. + noise_map + The noise-map of the positions which are used to compute the log likelihood of the positions. + tracer + The tracer of galaxies whose point source profile are used to fit the positions. + solver + Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing + triangles to and from the source-plane. + profile + Manually input the profile of the point source, which is used instead of the one extracted from the + tracer via name pairing if that profile is not found. + """ + + super().__init__( + name=name, + data=data, + noise_map=noise_map, + tracer=tracer, + solver=solver, + profile=profile, + xp=xp, + ) + + @staticmethod + def square_distance( + coord1: np.array, + coord2: np.array, + ) -> float: + """ + Calculate the square distance between two points. + + Parameters + ---------- + coord1 + The first point to calculate the distance between. + coord2 + The second point to calculate the distance between. + + Returns + ------- + The square distance between the two points + """ + return (coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2 + + @property + def model_data(self) -> aa.Grid2DIrregular: + """ + Returns the model positions, which are computed via the point solver. + """ + return self.solver.solve( + tracer=self.tracer, + source_plane_coordinate=self.source_plane_coordinate, + xp=self._xp, + plane_redshift=self.plane_redshift, + remove_infinities=False, + ) diff --git a/autolens/point/fit/positions/image/pair_all.py b/autolens/point/fit/positions/image/pair_all.py index 33e2c3858..cdcf27775 100644 --- a/autolens/point/fit/positions/image/pair_all.py +++ b/autolens/point/fit/positions/image/pair_all.py @@ -1,154 +1,154 @@ -import numpy as np - -from autolens.point.fit.positions.image.abstract import AbstractFitPositionsImagePair - - -class FitPositionsImagePairAll(AbstractFitPositionsImagePair): - """ - Fits the positions of a a point source dataset using a `Tracer` object with an image-plane chi-squared where every - model position of the point-source is paired with all other observed positions using the probability of each - model posiition explaining each observed position. - - Pairing all model positions with all observed positions is a less intuitive and commonly used approach - than other methods, for example pairing each position one-to-one. The scheme was proposed in the paper - below and provides a number of benefits, for example being a fully Bayesian approach to the problem and - linearizing aspects of the problem. - - https://arxiv.org/abs/2406.15280 - - THIS IMPLEMENTATION DOES NOT CURRRENTLY BREAK DOWN THE CALCULATION INTO A RESIDUAL MAP AND CHI-SQUARED, - GOING STRAIGHT TO A `log_likelihood`. FUTURE WORK WILL WORK OUT HOW TO EXPRESS THIS IN TERMS OF A CHI-SQUARED - AND RESIDUAL MAP. - - The fit performs the following steps: - - 1) Determine the source-plane centre of the point source, which could be a free model parameter or computed - as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). - - 2) Determine the image-plane model positions using the `PointSolver` and the source-plane centre of the point - source (e.g. ray tracing triangles to and from the image and source planes), including accounting for - multi-plane ray-tracing. - - 3) Pair every model position with every observed position and return the overall log likelihood of the fit. - - Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the - point source dataset to ensure that point source datasets are fitted to the correct point source. - - This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, - which may also fit other components of the point dataset like fluxes or time delays. - - When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` profile. - data - The positions of the point source in the image-plane which are fitted. - noise_map - The noise-map of the positions which are used to compute the log likelihood of the positions. - tracer - The tracer of galaxies whose point source profile are used to fit the positions. - solver - Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing - triangles to and from the source-plane. - profile - Manually input the profile of the point source, which is used instead of the one extracted from the - tracer via name pairing if that profile is not found. - """ - - def log_p( - self, - data_position: np.ndarray, - model_position: np.ndarray, - sigma: float, - ) -> float: - """ - Compute the log probability of a given model coordinate explaining a given observed coordinate. - - Accounts for noise, with noiser image coordinates having a comparatively lower log probability. - - Parameters - ---------- - data_position - The observed coordinate. - model_position - The model coordinate. - sigma - The noise associated with the observed coordinate. - - Returns - ------- - The log probability of the model coordinate explaining the observed coordinate. - """ - chi2 = self.square_distance(data_position, model_position) / sigma**2 - return -self._xp.log(self._xp.sqrt(2 * self._xp.pi * sigma**2)) - 0.5 * chi2 - - def all_permutations_log_likelihoods(self) -> np.ndarray: - """ - Compute the log likelihood for each permutation whereby the model could explain the observed image coordinates. - - For example, if there are two observed image coordinates and two model image coordinates, the log likelihood - for each permutation is: - - P(data_0 | model_0) * P(data_1 | model_1) - P(data_0 | model_1) * P(data_1 | model_0) - P(data_0 | model_0) * P(data_1 | model_0) - P(data_0 | model_1) * P(data_1 | model_1) - - This is every way in which the coordinates generated by the model can explain the observed coordinates. - """ - - model_data = self.model_data.array - - return self._xp.array( - [ - self._xp.log( - self._xp.sum( - self._xp.array( - [ - self._xp.exp( - self.log_p( - data_position, - model_position, - sigma, - ) - ) - for model_position in model_data - ] - ) - ) - ) - for data_position, sigma in zip(self.data, self.noise_map) - ] - ) - - @property - def chi_squared(self) -> float: - """ - Compute the log likelihood of the model image coordinates explaining the observed image coordinates. - - This is the sum across all permutations of the observed image coordinates of the log probability of each - model image coordinate explaining the observed image coordinate. - - For example, if there are two observed image coordinates and two model image coordinates, the log likelihood - is the sum of the log probabilities: - - P(data_0 | model_0) * P(data_1 | model_1) - + P(data_0 | model_1) * P(data_1 | model_0) - + P(data_0 | model_0) * P(data_1 | model_0) - + P(data_0 | model_1) * P(data_1 | model_1) - - This is every way in which the coordinates generated by the model can explain the observed coordinates. - """ - n_non_nan_model_positions = self._xp.count_nonzero( - self._xp.isfinite( - self.model_data.array, - ).any(axis=1) - ) - n_permutations = n_non_nan_model_positions ** len(self.data) - return -2.0 * ( - -self._xp.log(n_permutations) - + self._xp.sum(self.all_permutations_log_likelihoods()) - ) +import numpy as np + +from autolens.point.fit.positions.image.abstract import AbstractFitPositionsImagePair + + +class FitPositionsImagePairAll(AbstractFitPositionsImagePair): + """ + Fits the positions of a a point source dataset using a `Tracer` object with an image-plane chi-squared where every + model position of the point-source is paired with all other observed positions using the probability of each + model posiition explaining each observed position. + + Pairing all model positions with all observed positions is a less intuitive and commonly used approach + than other methods, for example pairing each position one-to-one. The scheme was proposed in the paper + below and provides a number of benefits, for example being a fully Bayesian approach to the problem and + linearizing aspects of the problem. + + https://arxiv.org/abs/2406.15280 + + THIS IMPLEMENTATION DOES NOT CURRRENTLY BREAK DOWN THE CALCULATION INTO A RESIDUAL MAP AND CHI-SQUARED, + GOING STRAIGHT TO A `log_likelihood`. FUTURE WORK WILL WORK OUT HOW TO EXPRESS THIS IN TERMS OF A CHI-SQUARED + AND RESIDUAL MAP. + + The fit performs the following steps: + + 1) Determine the source-plane centre of the point source, which could be a free model parameter or computed + as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). + + 2) Determine the image-plane model positions using the `PointSolver` and the source-plane centre of the point + source (e.g. ray tracing triangles to and from the image and source planes), including accounting for + multi-plane ray-tracing. + + 3) Pair every model position with every observed position and return the overall log likelihood of the fit. + + Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the + point source dataset to ensure that point source datasets are fitted to the correct point source. + + This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, + which may also fit other components of the point dataset like fluxes or time delays. + + When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` profile. + data + The positions of the point source in the image-plane which are fitted. + noise_map + The noise-map of the positions which are used to compute the log likelihood of the positions. + tracer + The tracer of galaxies whose point source profile are used to fit the positions. + solver + Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing + triangles to and from the source-plane. + profile + Manually input the profile of the point source, which is used instead of the one extracted from the + tracer via name pairing if that profile is not found. + """ + + def log_p( + self, + data_position: np.ndarray, + model_position: np.ndarray, + sigma: float, + ) -> float: + """ + Compute the log probability of a given model coordinate explaining a given observed coordinate. + + Accounts for noise, with noiser image coordinates having a comparatively lower log probability. + + Parameters + ---------- + data_position + The observed coordinate. + model_position + The model coordinate. + sigma + The noise associated with the observed coordinate. + + Returns + ------- + The log probability of the model coordinate explaining the observed coordinate. + """ + chi2 = self.square_distance(data_position, model_position) / sigma**2 + return -self._xp.log(self._xp.sqrt(2 * self._xp.pi * sigma**2)) - 0.5 * chi2 + + def all_permutations_log_likelihoods(self) -> np.ndarray: + """ + Compute the log likelihood for each permutation whereby the model could explain the observed image coordinates. + + For example, if there are two observed image coordinates and two model image coordinates, the log likelihood + for each permutation is: + + P(data_0 | model_0) * P(data_1 | model_1) + P(data_0 | model_1) * P(data_1 | model_0) + P(data_0 | model_0) * P(data_1 | model_0) + P(data_0 | model_1) * P(data_1 | model_1) + + This is every way in which the coordinates generated by the model can explain the observed coordinates. + """ + + model_data = self.model_data.array + + return self._xp.array( + [ + self._xp.log( + self._xp.sum( + self._xp.array( + [ + self._xp.exp( + self.log_p( + data_position, + model_position, + sigma, + ) + ) + for model_position in model_data + ] + ) + ) + ) + for data_position, sigma in zip(self.data, self.noise_map) + ] + ) + + @property + def chi_squared(self) -> float: + """ + Compute the log likelihood of the model image coordinates explaining the observed image coordinates. + + This is the sum across all permutations of the observed image coordinates of the log probability of each + model image coordinate explaining the observed image coordinate. + + For example, if there are two observed image coordinates and two model image coordinates, the log likelihood + is the sum of the log probabilities: + + P(data_0 | model_0) * P(data_1 | model_1) + + P(data_0 | model_1) * P(data_1 | model_0) + + P(data_0 | model_0) * P(data_1 | model_0) + + P(data_0 | model_1) * P(data_1 | model_1) + + This is every way in which the coordinates generated by the model can explain the observed coordinates. + """ + n_non_nan_model_positions = self._xp.count_nonzero( + self._xp.isfinite( + self.model_data.array, + ).any(axis=1) + ) + n_permutations = n_non_nan_model_positions ** len(self.data) + return -2.0 * ( + -self._xp.log(n_permutations) + + self._xp.sum(self.all_permutations_log_likelihoods()) + ) diff --git a/autolens/point/fit/positions/source/separations.py b/autolens/point/fit/positions/source/separations.py index 9919f3207..dbbedbe5a 100644 --- a/autolens/point/fit/positions/source/separations.py +++ b/autolens/point/fit/positions/source/separations.py @@ -1,163 +1,163 @@ -""" -Source-plane position fitting via traced-position separations. - -Instead of comparing predicted and observed positions in the image plane, -``FitPositionsSourcePlane`` traces each *observed* image position back to the source -plane via the tracer's deflection angles and measures how tightly they converge. - -If the lens model is correct, all observed images of the same source should trace back -to (approximately) the same source-plane coordinate. The figure of merit is the mean -squared separation of the back-traced positions from their common centroid, normalised by -the position noise map. - -This approach avoids the need for a ``PointSolver`` (no forward-solving is required) and -is well-suited to JAX-accelerated model fits. -""" -import numpy as np -from typing import Optional - -import autoarray as aa -import autogalaxy as ag - -from autolens.lens.tracer import Tracer -from autolens.point.fit.positions.abstract import AbstractFitPositions -from autolens.point.solver import PointSolver - - -class FitPositionsSource(AbstractFitPositions): - def __init__( - self, - name: str, - data: aa.Grid2DIrregular, - noise_map: aa.ArrayIrregular, - tracer: Tracer, - solver: Optional[PointSolver], - profile: Optional[ag.ps.Point] = None, - xp=np, - ): - """ - Fits the positions of a a point source dataset using a `Tracer` object with a source-plane chi-squared based on - the separation of image-plane positions ray-traced to the source-plane compared to the centre of the source - galaxy. - - The fit performs the following steps: - - 1) Determine the source-plane centre of the source-galaxy, which could be a free model parameter or computed - as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). - - 2) Ray-trace the positions in the point source to the source-plane via the `Tracer`, including accounting for - multi-plane ray-tracing. - - 3) Compute the distance of each ray-traced position to the source-plane centre and compute the residuals, - - 4) Compute the magnification of each image-plane position via the Hessian of the tracer's deflection angles. - - 5) Compute the residuals of each position as the difference between the source-plane centre and each - ray-traced position. - - 6) Compute the chi-squared of each position as the square of the residual multiplied by the magnification and - divided by the RMS noise-map value. - - 7) Sum the chi-squared values to compute the overall log likelihood of the fit. - - Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the - point source dataset to ensure that point source datasets are fitted to the correct point source. - - This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, - which may also fit other components of the point dataset like fluxes or time delays. - - When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object - is called and returned in the `log_likelihood_function`. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` profile. - data - The positions of the point source in the image-plane which are fitted. - noise_map - The noise-map of the positions which are used to compute the log likelihood of the positions. - tracer - The tracer of galaxies whose point source profile are used to fit the positions. - solver - Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing - triangles to and from the source-plane. This is not used in this source-plane point source fit. - profile - Manually input the profile of the point source, which is used instead of the one extracted from the - tracer via name pairing if that profile is not found. - """ - - super().__init__( - name=name, - data=data, - noise_map=noise_map, - tracer=tracer, - solver=solver, - profile=profile, - xp=xp, - ) - - @property - def model_data(self) -> aa.Grid2DIrregular: - """ - Returns the source-plane model positions of the point source, which are the positions of the image-plane - positions ray-traced to the source-plane. - - This calculation accounts for multi-plane ray-tracing, whereby if the tracer has more than 2 planees the - redshift of the point source galaxy is extracted and the deflections between the image-plane and source-plane - at its specific redshift are used. - """ - if len(self.tracer.planes) <= 2: - deflections = self.tracer.deflections_yx_2d_from( - grid=self.data, xp=self._xp - ) - else: - deflections = self.tracer.deflections_between_planes_from( - grid=self.data, xp=self._xp, plane_i=0, plane_j=self.plane_index - ) - - return self.data.grid_2d_via_deflection_grid_from(deflection_grid=deflections) - - @property - def residual_map(self) -> aa.ArrayIrregular: - """ - Returns the residuals of the point-source source-plane fit, which are the distances of each source-plane - position from the source-plane centre. - """ - return self.model_data.distances_to_coordinate_from( - coordinate=self.source_plane_coordinate - ) - - @property - def chi_squared_map(self) -> float: - """ - Returns the chi-squared of the point-source source-plane fit, which is the sum of the squared residuals - multiplied by the magnifications squared, divided by the noise-map values squared. - """ - - return self.residual_map**2.0 / ( - self.magnifications_at_positions.array**-2.0 * self.noise_map.array**2.0 - ) - - @property - def noise_normalization(self) -> float: - """ - Returns the normalization of the noise-map, which is the sum of the noise-map values squared. - """ - return self._xp.sum( - self._xp.log( - 2 - * np.pi - * ( - self.magnifications_at_positions.array**-2.0 - * self.noise_map.array**2.0 - ) - ) - ) - - @property - def log_likelihood(self) -> float: - """ - Returns the log likelihood of the point-source source-plane fit, which is the sum of the chi-squared values. - """ - return -0.5 * (sum(self.chi_squared_map) + self.noise_normalization) +""" +Source-plane position fitting via traced-position separations. + +Instead of comparing predicted and observed positions in the image plane, +``FitPositionsSourcePlane`` traces each *observed* image position back to the source +plane via the tracer's deflection angles and measures how tightly they converge. + +If the lens model is correct, all observed images of the same source should trace back +to (approximately) the same source-plane coordinate. The figure of merit is the mean +squared separation of the back-traced positions from their common centroid, normalised by +the position noise map. + +This approach avoids the need for a ``PointSolver`` (no forward-solving is required) and +is well-suited to JAX-accelerated model fits. +""" +import numpy as np +from typing import Optional + +import autoarray as aa +import autogalaxy as ag + +from autolens.lens.tracer import Tracer +from autolens.point.fit.positions.abstract import AbstractFitPositions +from autolens.point.solver import PointSolver + + +class FitPositionsSource(AbstractFitPositions): + def __init__( + self, + name: str, + data: aa.Grid2DIrregular, + noise_map: aa.ArrayIrregular, + tracer: Tracer, + solver: Optional[PointSolver], + profile: Optional[ag.ps.Point] = None, + xp=np, + ): + """ + Fits the positions of a a point source dataset using a `Tracer` object with a source-plane chi-squared based on + the separation of image-plane positions ray-traced to the source-plane compared to the centre of the source + galaxy. + + The fit performs the following steps: + + 1) Determine the source-plane centre of the source-galaxy, which could be a free model parameter or computed + as the barycenter of ray-traced positions in the source-plane, using name pairing (see below). + + 2) Ray-trace the positions in the point source to the source-plane via the `Tracer`, including accounting for + multi-plane ray-tracing. + + 3) Compute the distance of each ray-traced position to the source-plane centre and compute the residuals, + + 4) Compute the magnification of each image-plane position via the Hessian of the tracer's deflection angles. + + 5) Compute the residuals of each position as the difference between the source-plane centre and each + ray-traced position. + + 6) Compute the chi-squared of each position as the square of the residual multiplied by the magnification and + divided by the RMS noise-map value. + + 7) Sum the chi-squared values to compute the overall log likelihood of the fit. + + Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the + point source dataset to ensure that point source datasets are fitted to the correct point source. + + This fit object is used in the `FitPointDataset` to perform position based fitting of a `PointDataset`, + which may also fit other components of the point dataset like fluxes or time delays. + + When performing a `model-fit`via an `AnalysisPoint` object the `figure_of_merit` of this object + is called and returned in the `log_likelihood_function`. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` profile. + data + The positions of the point source in the image-plane which are fitted. + noise_map + The noise-map of the positions which are used to compute the log likelihood of the positions. + tracer + The tracer of galaxies whose point source profile are used to fit the positions. + solver + Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing + triangles to and from the source-plane. This is not used in this source-plane point source fit. + profile + Manually input the profile of the point source, which is used instead of the one extracted from the + tracer via name pairing if that profile is not found. + """ + + super().__init__( + name=name, + data=data, + noise_map=noise_map, + tracer=tracer, + solver=solver, + profile=profile, + xp=xp, + ) + + @property + def model_data(self) -> aa.Grid2DIrregular: + """ + Returns the source-plane model positions of the point source, which are the positions of the image-plane + positions ray-traced to the source-plane. + + This calculation accounts for multi-plane ray-tracing, whereby if the tracer has more than 2 planees the + redshift of the point source galaxy is extracted and the deflections between the image-plane and source-plane + at its specific redshift are used. + """ + if len(self.tracer.planes) <= 2: + deflections = self.tracer.deflections_yx_2d_from( + grid=self.data, xp=self._xp + ) + else: + deflections = self.tracer.deflections_between_planes_from( + grid=self.data, xp=self._xp, plane_i=0, plane_j=self.plane_index + ) + + return self.data.grid_2d_via_deflection_grid_from(deflection_grid=deflections) + + @property + def residual_map(self) -> aa.ArrayIrregular: + """ + Returns the residuals of the point-source source-plane fit, which are the distances of each source-plane + position from the source-plane centre. + """ + return self.model_data.distances_to_coordinate_from( + coordinate=self.source_plane_coordinate + ) + + @property + def chi_squared_map(self) -> float: + """ + Returns the chi-squared of the point-source source-plane fit, which is the sum of the squared residuals + multiplied by the magnifications squared, divided by the noise-map values squared. + """ + + return self.residual_map**2.0 / ( + self.magnifications_at_positions.array**-2.0 * self.noise_map.array**2.0 + ) + + @property + def noise_normalization(self) -> float: + """ + Returns the normalization of the noise-map, which is the sum of the noise-map values squared. + """ + return self._xp.sum( + self._xp.log( + 2 + * np.pi + * ( + self.magnifications_at_positions.array**-2.0 + * self.noise_map.array**2.0 + ) + ) + ) + + @property + def log_likelihood(self) -> float: + """ + Returns the log likelihood of the point-source source-plane fit, which is the sum of the chi-squared values. + """ + return -0.5 * (sum(self.chi_squared_map) + self.noise_normalization) diff --git a/autolens/point/fit/times_delays.py b/autolens/point/fit/times_delays.py index ca21e051e..3a43ca6a0 100644 --- a/autolens/point/fit/times_delays.py +++ b/autolens/point/fit/times_delays.py @@ -1,123 +1,123 @@ -""" -Time-delay fit component for point-source lensing. - -``FitTimeDelays`` computes the likelihood of the observed time delays between multiple -images of a point source given the Fermat potential predicted by the tracer's mass and -light models. - -The time delay between images i and j is: - - Δt_{ij} = (D_Δt / c) × [φ(θ_i) − φ(θ_j)] - -where D_Δt is the time-delay distance computed from the cosmology and φ is the Fermat -potential. A chi-squared is computed against the observed delays and their noise map, -contributing to the total ``FitPointDataset`` log likelihood. -""" -import numpy as np -from typing import Optional - -import autoarray as aa -import autogalaxy as ag - -from autolens.point.fit.abstract import AbstractFitPoint -from autolens.lens.tracer import Tracer - - -class FitTimeDelays(AbstractFitPoint): - def __init__( - self, - name: str, - data: aa.ArrayIrregular, - noise_map: aa.ArrayIrregular, - positions: aa.Grid2DIrregular, - tracer: Tracer, - profile: Optional[ag.ps.Point] = None, - xp=np, - ): - """ - Fits the time delays of a point source dataset using a `Tracer` object, - where every model time delay of the point-source is compared with its observed time delay. - - The fit performs the following steps: - - 1) Compute the model time delays at the input image-plane `positions` using the tracer. - - 2) Compute the relative time delays of the dataset time delays and the time delays of the point source at - these positions, which are the time delays relative to the shortest time delay - - 2) Subtract the observed relative time delays from the model relative time delays to compute the residuals. - - 3) Compute the chi-squared of each time delay residual. - - 4) Sum the chi-squared values to compute the overall log likelihood of the fit. - - Time delay fitting uses name pairing similar to flux fitting to ensure - the correct point source profile is used. - - Parameters - ---------- - name - The name of the point source dataset which is paired to a `Point` profile. - data - The observed time delays in days of the point source. - noise_map - The noise-map of the time delays in days used to compute the log likelihood. - tracer - The tracer of galaxies whose point source profile is used to fit the time delays. - positions - The image-plane positions of the point source where the time delays are calculated. - profile - Manually input the profile of the point source, used instead of one extracted from the tracer. - """ - self.positions = positions - - super().__init__( - name=name, - data=data, - noise_map=noise_map, - tracer=tracer, - solver=None, - profile=profile, - xp=xp, - ) - - @property - def model_data(self) -> aa.ArrayIrregular: - """ - The model time delays of the tracer at each of the input image-plane positions. - - These values are not subtracted by the shorter time delay of the point source, which would make the shorter - delay have a value of zero. However, this subtraction is performed in the `residual_map` property, in order - to ensure the residuals are computed relative to the shorter time delay. - """ - return self.tracer.time_delays_from(grid=self.positions, xp=self._xp) - - @property - def model_time_delays(self) -> aa.ArrayIrregular: - return self.model_data - - @property - def residual_map(self) -> aa.ArrayIrregular: - """ - Returns the difference between the observed and model time delays of the point source, - which is the residual time delay of the fit. - - The residuals are computed relative to the shortest time delay of the point source, which is subtracted - from the dataset time delays and model time delays before the subtraction. - """ - - data = self.data - self._xp.min(self.data.array) - model_data = self.model_data - self._xp.min(self.model_data.array) - - residual_map = aa.util.fit.residual_map_from(data=data, model_data=model_data) - return aa.ArrayIrregular(values=residual_map) - - @property - def chi_squared(self) -> float: - """ - Returns the chi-squared of the fit of the point source time delays, - which is the residual values divided by the RMS noise-map squared. - """ - return ag.util.fit.chi_squared_from( - chi_squared_map=self.chi_squared_map.array, - ) +""" +Time-delay fit component for point-source lensing. + +``FitTimeDelays`` computes the likelihood of the observed time delays between multiple +images of a point source given the Fermat potential predicted by the tracer's mass and +light models. + +The time delay between images i and j is: + + Δt_{ij} = (D_Δt / c) × [φ(θ_i) − φ(θ_j)] + +where D_Δt is the time-delay distance computed from the cosmology and φ is the Fermat +potential. A chi-squared is computed against the observed delays and their noise map, +contributing to the total ``FitPointDataset`` log likelihood. +""" +import numpy as np +from typing import Optional + +import autoarray as aa +import autogalaxy as ag + +from autolens.point.fit.abstract import AbstractFitPoint +from autolens.lens.tracer import Tracer + + +class FitTimeDelays(AbstractFitPoint): + def __init__( + self, + name: str, + data: aa.ArrayIrregular, + noise_map: aa.ArrayIrregular, + positions: aa.Grid2DIrregular, + tracer: Tracer, + profile: Optional[ag.ps.Point] = None, + xp=np, + ): + """ + Fits the time delays of a point source dataset using a `Tracer` object, + where every model time delay of the point-source is compared with its observed time delay. + + The fit performs the following steps: + + 1) Compute the model time delays at the input image-plane `positions` using the tracer. + + 2) Compute the relative time delays of the dataset time delays and the time delays of the point source at + these positions, which are the time delays relative to the shortest time delay + + 2) Subtract the observed relative time delays from the model relative time delays to compute the residuals. + + 3) Compute the chi-squared of each time delay residual. + + 4) Sum the chi-squared values to compute the overall log likelihood of the fit. + + Time delay fitting uses name pairing similar to flux fitting to ensure + the correct point source profile is used. + + Parameters + ---------- + name + The name of the point source dataset which is paired to a `Point` profile. + data + The observed time delays in days of the point source. + noise_map + The noise-map of the time delays in days used to compute the log likelihood. + tracer + The tracer of galaxies whose point source profile is used to fit the time delays. + positions + The image-plane positions of the point source where the time delays are calculated. + profile + Manually input the profile of the point source, used instead of one extracted from the tracer. + """ + self.positions = positions + + super().__init__( + name=name, + data=data, + noise_map=noise_map, + tracer=tracer, + solver=None, + profile=profile, + xp=xp, + ) + + @property + def model_data(self) -> aa.ArrayIrregular: + """ + The model time delays of the tracer at each of the input image-plane positions. + + These values are not subtracted by the shorter time delay of the point source, which would make the shorter + delay have a value of zero. However, this subtraction is performed in the `residual_map` property, in order + to ensure the residuals are computed relative to the shorter time delay. + """ + return self.tracer.time_delays_from(grid=self.positions, xp=self._xp) + + @property + def model_time_delays(self) -> aa.ArrayIrregular: + return self.model_data + + @property + def residual_map(self) -> aa.ArrayIrregular: + """ + Returns the difference between the observed and model time delays of the point source, + which is the residual time delay of the fit. + + The residuals are computed relative to the shortest time delay of the point source, which is subtracted + from the dataset time delays and model time delays before the subtraction. + """ + + data = self.data - self._xp.min(self.data.array) + model_data = self.model_data - self._xp.min(self.model_data.array) + + residual_map = aa.util.fit.residual_map_from(data=data, model_data=model_data) + return aa.ArrayIrregular(values=residual_map) + + @property + def chi_squared(self) -> float: + """ + Returns the chi-squared of the fit of the point source time delays, + which is the residual values divided by the RMS noise-map squared. + """ + return ag.util.fit.chi_squared_from( + chi_squared_map=self.chi_squared_map.array, + ) diff --git a/autolens/point/max_separation.py b/autolens/point/max_separation.py index dd87da170..411771014 100644 --- a/autolens/point/max_separation.py +++ b/autolens/point/max_separation.py @@ -1,90 +1,90 @@ -""" -Maximum source-plane separation statistic for point-source position fitting. - -``SourceMaxSeparation`` computes the maximum separation between the source-plane -positions obtained by tracing each observed image-plane position through the lens model. -For a perfect lens model all images of the same source trace back to exactly the same -source-plane coordinate, so this maximum separation should be zero. - -This statistic is used as a fast figure-of-merit in settings where a full chi-squared -calculation is not required, or as a hard prior threshold that rejects models for which -the back-traced positions diverge by more than a user-specified amount. -""" -import numpy as np -from typing import Optional - -import autoarray as aa - -from autolens.lens.tracer import Tracer - - -class SourceMaxSeparation: - def __init__( - self, - data: aa.Grid2DIrregular, - noise_map: Optional[aa.ArrayIrregular], - tracer: Tracer, - plane_redshift: float = Optional[None], - xp=np, - ): - """ - Given a positions dataset, which is a list of positions with names that associated them to model source - galaxies, use a `Tracer` to determine the traced coordinate positions in the source-plane. - - Different children of this abstract class are available which use the traced coordinates to define a chi-squared - value in different ways. - - Parameters - ---------- - data : Grid2DIrregular - The (y,x) arc-second coordinates of named positions which the log_likelihood is computed using. Positions - are paired to galaxies in the `Tracer` using their names. - tracer : Tracer - The object that defines the ray-tracing of the strong lens system of galaxies. - noise_value - The noise-value assumed when computing the log likelihood. - plane_redshift - The redshift of the plane in the `Tracer` that the source-plane positions are computed from. This is - often the last plane in the `Tracer`, which is the source-plane. - """ - - self.data = data - self.noise_map = noise_map - - try: - plane_index = tracer.plane_index_via_redshift_from(redshift=plane_redshift) - except TypeError: - plane_index = -1 - - self.plane_positions = aa.Grid2DIrregular( - values=tracer.traced_grid_2d_list_from(grid=data, xp=xp)[plane_index], xp=xp - ) - - @property - def furthest_separations_of_plane_positions(self) -> aa.ArrayIrregular: - """ - Returns the furthest distance of every source-plane (y,x) coordinate to the other source-plane (y,x) - coordinates. - - For example, for the following plane positions: - - plane_positions = [[(0.0, 0.0), (0.0, 1.0), (0.0, 3.0)] - - The returned furthest distances are: - - plane_positions = [3.0, 2.0, 3.0] - - Returns - ------- - aa.ArrayIrregular - The further distances of every set of grouped source-plane coordinates the other source-plane coordinates - that it is grouped with. - """ - return self.plane_positions.furthest_distances_to_other_coordinates - - @property - def max_separation_of_plane_positions(self) -> float: - return max(self.furthest_separations_of_plane_positions) - - def max_separation_within_threshold(self, threshold) -> bool: - return self.max_separation_of_plane_positions <= threshold +""" +Maximum source-plane separation statistic for point-source position fitting. + +``SourceMaxSeparation`` computes the maximum separation between the source-plane +positions obtained by tracing each observed image-plane position through the lens model. +For a perfect lens model all images of the same source trace back to exactly the same +source-plane coordinate, so this maximum separation should be zero. + +This statistic is used as a fast figure-of-merit in settings where a full chi-squared +calculation is not required, or as a hard prior threshold that rejects models for which +the back-traced positions diverge by more than a user-specified amount. +""" +import numpy as np +from typing import Optional + +import autoarray as aa + +from autolens.lens.tracer import Tracer + + +class SourceMaxSeparation: + def __init__( + self, + data: aa.Grid2DIrregular, + noise_map: Optional[aa.ArrayIrregular], + tracer: Tracer, + plane_redshift: float = Optional[None], + xp=np, + ): + """ + Given a positions dataset, which is a list of positions with names that associated them to model source + galaxies, use a `Tracer` to determine the traced coordinate positions in the source-plane. + + Different children of this abstract class are available which use the traced coordinates to define a chi-squared + value in different ways. + + Parameters + ---------- + data : Grid2DIrregular + The (y,x) arc-second coordinates of named positions which the log_likelihood is computed using. Positions + are paired to galaxies in the `Tracer` using their names. + tracer : Tracer + The object that defines the ray-tracing of the strong lens system of galaxies. + noise_value + The noise-value assumed when computing the log likelihood. + plane_redshift + The redshift of the plane in the `Tracer` that the source-plane positions are computed from. This is + often the last plane in the `Tracer`, which is the source-plane. + """ + + self.data = data + self.noise_map = noise_map + + try: + plane_index = tracer.plane_index_via_redshift_from(redshift=plane_redshift) + except TypeError: + plane_index = -1 + + self.plane_positions = aa.Grid2DIrregular( + values=tracer.traced_grid_2d_list_from(grid=data, xp=xp)[plane_index], xp=xp + ) + + @property + def furthest_separations_of_plane_positions(self) -> aa.ArrayIrregular: + """ + Returns the furthest distance of every source-plane (y,x) coordinate to the other source-plane (y,x) + coordinates. + + For example, for the following plane positions: + + plane_positions = [[(0.0, 0.0), (0.0, 1.0), (0.0, 3.0)] + + The returned furthest distances are: + + plane_positions = [3.0, 2.0, 3.0] + + Returns + ------- + aa.ArrayIrregular + The further distances of every set of grouped source-plane coordinates the other source-plane coordinates + that it is grouped with. + """ + return self.plane_positions.furthest_distances_to_other_coordinates + + @property + def max_separation_of_plane_positions(self) -> float: + return max(self.furthest_separations_of_plane_positions) + + def max_separation_within_threshold(self, threshold) -> bool: + return self.max_separation_of_plane_positions <= threshold diff --git a/autolens/point/mock/mock_solver.py b/autolens/point/mock/mock_solver.py index 449fa42fe..9685d8ad3 100644 --- a/autolens/point/mock/mock_solver.py +++ b/autolens/point/mock/mock_solver.py @@ -1,18 +1,18 @@ -from typing import Optional - -import numpy as np - - -class MockPointSolver: - def __init__(self, model_positions): - self.model_positions = model_positions - - def solve( - self, - tracer, - source_plane_coordinate, - xp=np, - plane_redshift: Optional[float] = None, - remove_infinities: bool = True, - ): - return self.model_positions +from typing import Optional + +import numpy as np + + +class MockPointSolver: + def __init__(self, model_positions): + self.model_positions = model_positions + + def solve( + self, + tracer, + source_plane_coordinate, + xp=np, + plane_redshift: Optional[float] = None, + remove_infinities: bool = True, + ): + return self.model_positions diff --git a/autolens/point/model/analysis.py b/autolens/point/model/analysis.py index c3da8bb6e..4a7d4a3b2 100644 --- a/autolens/point/model/analysis.py +++ b/autolens/point/model/analysis.py @@ -1,243 +1,243 @@ -""" -Analysis class for fitting a ``Tracer`` model to a point-source dataset. - -``AnalysisPoint`` implements the ``log_likelihood_function`` called by a ``PyAutoFit`` -non-linear search at each iteration. It: - -1. Constructs a ``Tracer`` from the current model instance. -2. Calls ``FitPointDataset`` to fit the point-source positions (and optionally fluxes - and time delays) using the ``PointSolver`` to find predicted image positions. -3. Optionally adds a position-based prior via ``PositionsLH`` that penalises models - where image positions are not self-consistent. -4. Returns the total log likelihood as the figure of merit. - -It also manages result output (``ResultPoint``) and on-the-fly visualisation -(``VisualizerPoint``). -""" -import numpy as np - -import autofit as af -import autogalaxy as ag - -from autogalaxy.analysis.analysis.analysis import Analysis as AgAnalysis - -from autolens.analysis.analysis.lens import AnalysisLens -from autolens.analysis.exceptions import raise_fit_exception -from autolens.point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat -from autolens.point.fit.dataset import FitPointDataset -from autolens.point.dataset import PointDataset -from autolens.point.model.result import ResultPoint -from autolens.point.model.visualizer import VisualizerPoint -from autolens.point.solver import PointSolver - - -class AnalysisPoint(AgAnalysis, AnalysisLens): - Visualizer = VisualizerPoint - Result = ResultPoint - - def __init__( - self, - dataset: PointDataset, - solver: PointSolver, - fit_positions_cls=FitPositionsImagePairRepeat, - image=None, - cosmology: ag.cosmo.LensingCosmology = None, - title_prefix: str = None, - use_jax: bool = True, - **kwargs, - ): - """ - Fits a lens model to a point source dataset (e.g. positions, fluxes, time delays) via a non-linear search. - - The `Analysis` class defines the `log_likelihood_function` which fits the model to the dataset and returns the - log likelihood value defining how well the model fitted the data. - - It handles many other tasks, such as visualization, outputting results to hard-disk and storing results in - a format that can be loaded after the model-fit is complete. - - This class is used for model-fits which fit lens models to point datasets, which may include some combination - of positions, fluxes and time-delays. - - This class stores the settings used to perform the model-fit for certain components of the model (e.g. a - pixelization or inversion), the Cosmology used for the analysis and adapt images used for certain model - classes. - - Parameters - ---------- - dataset - The `PointDataset` that is fitted by the model, which contains a combination of positions, fluxes and - time-delays. - solver - Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing - triangles to and from the source-plane. - fit_positions_cls - The class used to fit the positions of the point source dataset, which could be an image-plane or - source-plane chi-squared. - cosmology - The Cosmology assumed for this analysis. - title_prefix - A string that is added before the title of all figures output by visualization, for example to - put the name of the dataset and galaxy in the title. - """ - super().__init__(cosmology=cosmology, use_jax=use_jax, **kwargs) - - # `super().__init__` (af.Analysis) is the single reader of the - # disable-jax env var + the jax-availability check; forward the - # resolved `self._use_jax` so `AnalysisLens` does not overwrite it. - AnalysisLens.__init__(self=self, cosmology=cosmology, use_jax=self._use_jax) - - self.dataset = dataset - - self.solver = solver - self.fit_positions_cls = fit_positions_cls - self.title_prefix = title_prefix - - def log_likelihood_function(self, instance): - """ - Given an instance of the model, where the model parameters are set via a non-linear search, fit the model - instance to the point source dataset. - - This function returns a log likelihood which is used by the non-linear search to guide the model-fit. - - For this analysis class, this function performs the following steps: - - 1) Extracts all galaxies from the model instance and set up a `Tracer`, which includes ordering the galaxies - by redshift to set up each `Plane`. - - 2) Use the `Tracer` and other attributes to create a `FitPointDataset` object, which performs the steps - below to fit different components of the point source dataset. - - 3) If the point source dataset has positions and model fits positions, perform this fit and compute the - log likelihood. This calculation uses the `fit_positions_cls` object, which may be an image-plane or - source-plane chi-squared. - - 4) If the point source dataset has fluxes and model fits fluxes, perform this fit and compute the log likelihood. - - 5) If the point source dataset has time-delays and model fits time-delays, perform this fit and compute the - log likelihood [NOT SUPPORTED YET]. - - 6) Sum the log likelihoods of the positions, fluxes and time-delays (if they are fitted) to get the overall - log likelihood of the model. - - Certain models will fail to fit the dataset and raise an exception. For example for ill defined mass models - the `PointSolver` may find no solution. In such circumstances the model is discarded and its likelihood value - is passed to the non-linear search in a way that it ignores it (for example, using a value of -1.0e99). - - Parameters - ---------- - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - - Returns - ------- - float - The log likelihood indicating how well this model instance fitted the imaging data. - """ - if self._use_jax: - return self.fit_from(instance=instance).log_likelihood - - try: - return self.fit_from(instance=instance).log_likelihood - except Exception as e: - raise_fit_exception(e) - - def fit_from( - self, - instance, - ) -> FitPointDataset: - """ - Given a model instance create a `FitPointDataset` object. - - This function is used in the `log_likelihood_function` to fit the model to the imaging data and compute the - log likelihood. - - Parameters - ---------- - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - - Returns - ------- - The fit of the lens model to the point source dataset. - """ - - if self._use_jax: - self._register_fit_point_pytrees() - - tracer = self.tracer_via_instance_from( - instance=instance, - ) - - return FitPointDataset( - dataset=self.dataset, - tracer=tracer, - solver=self.solver, - fit_positions_cls=self.fit_positions_cls, - xp=self._xp, - ) - - @staticmethod - def _register_fit_point_pytrees() -> None: - """Register every type reachable from a ``FitPointDataset`` return value - so ``jax.jit(fit_from)`` can flatten its output. - - ``dataset`` and ``solver`` are constants per analysis — ride as aux so - JAX does not recurse into them. ``fit_positions_cls`` is a class reference - (not a value) so must also ride as aux. ``tracer`` is dynamic per fit. - """ - from autoarray.abstract_ndarray import register_instance_pytree - from autolens.lens.tracer import Tracer - from autolens.point.fit.positions.image.pair_all import FitPositionsImagePairAll - from autolens.point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat - from autolens.point.fit.positions.image.pair import FitPositionsImagePair - import autogalaxy as ag - - register_instance_pytree( - FitPointDataset, - no_flatten=("dataset", "solver", "fit_positions_cls"), - ) - register_instance_pytree(Tracer, no_flatten=("cosmology",)) - # fit-point-pytree: observed data/noise are per-analysis constants; solver/name/use_jax are non-JAX - register_instance_pytree( - FitPositionsImagePairAll, - no_flatten=("solver", "name", "use_jax", "_data", "_noise_map"), - ) - # fit-point-pytree - register_instance_pytree( - FitPositionsImagePairRepeat, - no_flatten=("solver", "name", "use_jax", "_data", "_noise_map"), - ) - # fit-point-pytree - register_instance_pytree( - FitPositionsImagePair, - no_flatten=("solver", "name", "use_jax", "_data", "_noise_map"), - ) - # fit-point-pytree: ag.ps.Point / PointFlux are handled by - # autofit.jax.pytrees.register_model before jit is called; skip here. - - def save_attributes(self, paths: af.DirectoryPaths): - """ - Before the non-linear search begins, this routine saves attributes of the `Analysis` object to the `files` - folder such that they can be loaded after the analysis using PyAutoFit's database and aggregator tools. - - For this analysis, it uses the `AnalysisDataset` object's method to output the following: - - - The dataset's point source dataset as a readable .json file. - - It is common for these attributes to be loaded by many of the template aggregator functions given in the - `aggregator` modules. For example, when using the database tools to perform a fit, the default behaviour is for - the dataset, settings and other attributes necessary to perform the fit to be loaded via the pickle files - output by this function. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization, and the pickled objects used by the aggregator output by this function. - """ - ag.output_to_json( - obj=self.dataset, - file_path=paths._files_path / "dataset.json", - ) +""" +Analysis class for fitting a ``Tracer`` model to a point-source dataset. + +``AnalysisPoint`` implements the ``log_likelihood_function`` called by a ``PyAutoFit`` +non-linear search at each iteration. It: + +1. Constructs a ``Tracer`` from the current model instance. +2. Calls ``FitPointDataset`` to fit the point-source positions (and optionally fluxes + and time delays) using the ``PointSolver`` to find predicted image positions. +3. Optionally adds a position-based prior via ``PositionsLH`` that penalises models + where image positions are not self-consistent. +4. Returns the total log likelihood as the figure of merit. + +It also manages result output (``ResultPoint``) and on-the-fly visualisation +(``VisualizerPoint``). +""" +import numpy as np + +import autofit as af +import autogalaxy as ag + +from autogalaxy.analysis.analysis.analysis import Analysis as AgAnalysis + +from autolens.analysis.analysis.lens import AnalysisLens +from autolens.analysis.exceptions import raise_fit_exception +from autolens.point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat +from autolens.point.fit.dataset import FitPointDataset +from autolens.point.dataset import PointDataset +from autolens.point.model.result import ResultPoint +from autolens.point.model.visualizer import VisualizerPoint +from autolens.point.solver import PointSolver + + +class AnalysisPoint(AgAnalysis, AnalysisLens): + Visualizer = VisualizerPoint + Result = ResultPoint + + def __init__( + self, + dataset: PointDataset, + solver: PointSolver, + fit_positions_cls=FitPositionsImagePairRepeat, + image=None, + cosmology: ag.cosmo.LensingCosmology = None, + title_prefix: str = None, + use_jax: bool = True, + **kwargs, + ): + """ + Fits a lens model to a point source dataset (e.g. positions, fluxes, time delays) via a non-linear search. + + The `Analysis` class defines the `log_likelihood_function` which fits the model to the dataset and returns the + log likelihood value defining how well the model fitted the data. + + It handles many other tasks, such as visualization, outputting results to hard-disk and storing results in + a format that can be loaded after the model-fit is complete. + + This class is used for model-fits which fit lens models to point datasets, which may include some combination + of positions, fluxes and time-delays. + + This class stores the settings used to perform the model-fit for certain components of the model (e.g. a + pixelization or inversion), the Cosmology used for the analysis and adapt images used for certain model + classes. + + Parameters + ---------- + dataset + The `PointDataset` that is fitted by the model, which contains a combination of positions, fluxes and + time-delays. + solver + Solves the lens equation in order to determine the image-plane positions of a point source by ray-tracing + triangles to and from the source-plane. + fit_positions_cls + The class used to fit the positions of the point source dataset, which could be an image-plane or + source-plane chi-squared. + cosmology + The Cosmology assumed for this analysis. + title_prefix + A string that is added before the title of all figures output by visualization, for example to + put the name of the dataset and galaxy in the title. + """ + super().__init__(cosmology=cosmology, use_jax=use_jax, **kwargs) + + # `super().__init__` (af.Analysis) is the single reader of the + # disable-jax env var + the jax-availability check; forward the + # resolved `self._use_jax` so `AnalysisLens` does not overwrite it. + AnalysisLens.__init__(self=self, cosmology=cosmology, use_jax=self._use_jax) + + self.dataset = dataset + + self.solver = solver + self.fit_positions_cls = fit_positions_cls + self.title_prefix = title_prefix + + def log_likelihood_function(self, instance): + """ + Given an instance of the model, where the model parameters are set via a non-linear search, fit the model + instance to the point source dataset. + + This function returns a log likelihood which is used by the non-linear search to guide the model-fit. + + For this analysis class, this function performs the following steps: + + 1) Extracts all galaxies from the model instance and set up a `Tracer`, which includes ordering the galaxies + by redshift to set up each `Plane`. + + 2) Use the `Tracer` and other attributes to create a `FitPointDataset` object, which performs the steps + below to fit different components of the point source dataset. + + 3) If the point source dataset has positions and model fits positions, perform this fit and compute the + log likelihood. This calculation uses the `fit_positions_cls` object, which may be an image-plane or + source-plane chi-squared. + + 4) If the point source dataset has fluxes and model fits fluxes, perform this fit and compute the log likelihood. + + 5) If the point source dataset has time-delays and model fits time-delays, perform this fit and compute the + log likelihood [NOT SUPPORTED YET]. + + 6) Sum the log likelihoods of the positions, fluxes and time-delays (if they are fitted) to get the overall + log likelihood of the model. + + Certain models will fail to fit the dataset and raise an exception. For example for ill defined mass models + the `PointSolver` may find no solution. In such circumstances the model is discarded and its likelihood value + is passed to the non-linear search in a way that it ignores it (for example, using a value of -1.0e99). + + Parameters + ---------- + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + + Returns + ------- + float + The log likelihood indicating how well this model instance fitted the imaging data. + """ + if self._use_jax: + return self.fit_from(instance=instance).log_likelihood + + try: + return self.fit_from(instance=instance).log_likelihood + except Exception as e: + raise_fit_exception(e) + + def fit_from( + self, + instance, + ) -> FitPointDataset: + """ + Given a model instance create a `FitPointDataset` object. + + This function is used in the `log_likelihood_function` to fit the model to the imaging data and compute the + log likelihood. + + Parameters + ---------- + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + + Returns + ------- + The fit of the lens model to the point source dataset. + """ + + if self._use_jax: + self._register_fit_point_pytrees() + + tracer = self.tracer_via_instance_from( + instance=instance, + ) + + return FitPointDataset( + dataset=self.dataset, + tracer=tracer, + solver=self.solver, + fit_positions_cls=self.fit_positions_cls, + xp=self._xp, + ) + + @staticmethod + def _register_fit_point_pytrees() -> None: + """Register every type reachable from a ``FitPointDataset`` return value + so ``jax.jit(fit_from)`` can flatten its output. + + ``dataset`` and ``solver`` are constants per analysis — ride as aux so + JAX does not recurse into them. ``fit_positions_cls`` is a class reference + (not a value) so must also ride as aux. ``tracer`` is dynamic per fit. + """ + from autoarray.abstract_ndarray import register_instance_pytree + from autolens.lens.tracer import Tracer + from autolens.point.fit.positions.image.pair_all import FitPositionsImagePairAll + from autolens.point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat + from autolens.point.fit.positions.image.pair import FitPositionsImagePair + import autogalaxy as ag + + register_instance_pytree( + FitPointDataset, + no_flatten=("dataset", "solver", "fit_positions_cls"), + ) + register_instance_pytree(Tracer, no_flatten=("cosmology",)) + # fit-point-pytree: observed data/noise are per-analysis constants; solver/name/use_jax are non-JAX + register_instance_pytree( + FitPositionsImagePairAll, + no_flatten=("solver", "name", "use_jax", "_data", "_noise_map"), + ) + # fit-point-pytree + register_instance_pytree( + FitPositionsImagePairRepeat, + no_flatten=("solver", "name", "use_jax", "_data", "_noise_map"), + ) + # fit-point-pytree + register_instance_pytree( + FitPositionsImagePair, + no_flatten=("solver", "name", "use_jax", "_data", "_noise_map"), + ) + # fit-point-pytree: ag.ps.Point / PointFlux are handled by + # autofit.jax.pytrees.register_model before jit is called; skip here. + + def save_attributes(self, paths: af.DirectoryPaths): + """ + Before the non-linear search begins, this routine saves attributes of the `Analysis` object to the `files` + folder such that they can be loaded after the analysis using PyAutoFit's database and aggregator tools. + + For this analysis, it uses the `AnalysisDataset` object's method to output the following: + + - The dataset's point source dataset as a readable .json file. + + It is common for these attributes to be loaded by many of the template aggregator functions given in the + `aggregator` modules. For example, when using the database tools to perform a fit, the default behaviour is for + the dataset, settings and other attributes necessary to perform the fit to be loaded via the pickle files + output by this function. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization, and the pickled objects used by the aggregator output by this function. + """ + ag.output_to_json( + obj=self.dataset, + file_path=paths._files_path / "dataset.json", + ) diff --git a/autolens/point/model/result.py b/autolens/point/model/result.py index 642cc2487..f005342e4 100644 --- a/autolens/point/model/result.py +++ b/autolens/point/model/result.py @@ -1,13 +1,13 @@ -import autoarray as aa - -from autolens.analysis.result import Result - - -class ResultPoint(Result): - @property - def grid(self): - return aa.Grid2D.uniform(shape_native=(100, 100), pixel_scales=0.1) - - @property - def max_log_likelihood_fit(self): - return self.analysis.fit_from(instance=self.instance) +import autoarray as aa + +from autolens.analysis.result import Result + + +class ResultPoint(Result): + @property + def grid(self): + return aa.Grid2D.uniform(shape_native=(100, 100), pixel_scales=0.1) + + @property + def max_log_likelihood_fit(self): + return self.analysis.fit_from(instance=self.instance) diff --git a/autolens/point/model/visualizer.py b/autolens/point/model/visualizer.py index fc3a985cc..8de71210a 100644 --- a/autolens/point/model/visualizer.py +++ b/autolens/point/model/visualizer.py @@ -1,107 +1,107 @@ -import autofit as af -import autogalaxy as ag - -from autolens.point.model.plotter import PlotterPoint -from autolens.imaging.plot.fit_imaging_plots import _compute_critical_curve_lines - - -class VisualizerPoint(af.Visualizer): - @staticmethod - def visualize_before_fit( - analysis, - paths: af.AbstractPaths, - model: af.AbstractPriorModel, - ): - """ - PyAutoFit calls this function immediately before the non-linear search begins. - - It visualizes objects which do not change throughout the model fit like the dataset. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization and the pickled objects used by the aggregator output by this function. - model - The model object, which includes model components representing the galaxies that are fitted to - the imaging data. - """ - - plotter = PlotterPoint( - image_path=paths.image_path, title_prefix=analysis.title_prefix - ) - - plotter.dataset_point(dataset=analysis.dataset) - - @staticmethod - def visualize( - analysis, - paths: af.DirectoryPaths, - instance: af.ModelInstance, - during_analysis: bool, - quick_update: bool = False, - ): - """ - Output images of the maximum log likelihood model inferred by the model-fit. This function is called throughout - the non-linear search at regular intervals, and therefore provides on-the-fly visualization of how well the - model-fit is going. - - The visualization performed by this function includes: - - - Images of the best-fit `Tracer`, including the images of each of its galaxies. - - - Images of the best-fit `FitPointDataset`, including the model-image, residuals and chi-squared of its fit to - the imaging data. - - The images output by this function are customized using the file `config/visualize/plots.yaml`. - - Parameters - ---------- - paths - The paths object which manages all paths, e.g. where the non-linear search outputs are stored, - visualization, and the pickled objects used by the aggregator output by this function. - instance - An instance of the model that is being fitted to the data by this analysis (whose parameters have been set - via a non-linear search). - """ - fit = analysis.fit_for_visualization(instance=instance) - - plotter = PlotterPoint( - image_path=paths.image_path, title_prefix=analysis.title_prefix - ) - - tracer = fit.tracer - - grid = ag.Grid2D.from_extent( - extent=fit.dataset.extent_from(), shape_native=(100, 100) - ) - - # Compute critical curves and caustics once for all plot functions. - ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curve_lines( - tracer, grid - ) - - plotter.fit_point( - fit=fit, - quick_update=quick_update, - image_plane_lines=ip_lines, - image_plane_line_colors=ip_colors, - source_plane_lines=sp_lines, - source_plane_line_colors=sp_colors, - ) - - if quick_update: - return - - plotter.tracer( - tracer=tracer, - grid=grid, - image_plane_lines=ip_lines, - image_plane_line_colors=ip_colors, - source_plane_lines=sp_lines, - source_plane_line_colors=sp_colors, - ) - plotter.galaxies( - galaxies=tracer.galaxies, - grid=grid, - ) +import autofit as af +import autogalaxy as ag + +from autolens.point.model.plotter import PlotterPoint +from autolens.imaging.plot.fit_imaging_plots import _compute_critical_curve_lines + + +class VisualizerPoint(af.Visualizer): + @staticmethod + def visualize_before_fit( + analysis, + paths: af.AbstractPaths, + model: af.AbstractPriorModel, + ): + """ + PyAutoFit calls this function immediately before the non-linear search begins. + + It visualizes objects which do not change throughout the model fit like the dataset. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + model + The model object, which includes model components representing the galaxies that are fitted to + the imaging data. + """ + + plotter = PlotterPoint( + image_path=paths.image_path, title_prefix=analysis.title_prefix + ) + + plotter.dataset_point(dataset=analysis.dataset) + + @staticmethod + def visualize( + analysis, + paths: af.DirectoryPaths, + instance: af.ModelInstance, + during_analysis: bool, + quick_update: bool = False, + ): + """ + Output images of the maximum log likelihood model inferred by the model-fit. This function is called throughout + the non-linear search at regular intervals, and therefore provides on-the-fly visualization of how well the + model-fit is going. + + The visualization performed by this function includes: + + - Images of the best-fit `Tracer`, including the images of each of its galaxies. + + - Images of the best-fit `FitPointDataset`, including the model-image, residuals and chi-squared of its fit to + the imaging data. + + The images output by this function are customized using the file `config/visualize/plots.yaml`. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization, and the pickled objects used by the aggregator output by this function. + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + """ + fit = analysis.fit_for_visualization(instance=instance) + + plotter = PlotterPoint( + image_path=paths.image_path, title_prefix=analysis.title_prefix + ) + + tracer = fit.tracer + + grid = ag.Grid2D.from_extent( + extent=fit.dataset.extent_from(), shape_native=(100, 100) + ) + + # Compute critical curves and caustics once for all plot functions. + ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curve_lines( + tracer, grid + ) + + plotter.fit_point( + fit=fit, + quick_update=quick_update, + image_plane_lines=ip_lines, + image_plane_line_colors=ip_colors, + source_plane_lines=sp_lines, + source_plane_line_colors=sp_colors, + ) + + if quick_update: + return + + plotter.tracer( + tracer=tracer, + grid=grid, + image_plane_lines=ip_lines, + image_plane_line_colors=ip_colors, + source_plane_lines=sp_lines, + source_plane_line_colors=sp_colors, + ) + plotter.galaxies( + galaxies=tracer.galaxies, + grid=grid, + ) diff --git a/autolens/point/solver/__init__.py b/autolens/point/solver/__init__.py index d9cf14301..2751beef9 100644 --- a/autolens/point/solver/__init__.py +++ b/autolens/point/solver/__init__.py @@ -1 +1 @@ -from .point_solver import PointSolver +from .point_solver import PointSolver diff --git a/autolens/point/solver/point_solver.py b/autolens/point/solver/point_solver.py index 30bb8e24c..dfb3e393c 100644 --- a/autolens/point/solver/point_solver.py +++ b/autolens/point/solver/point_solver.py @@ -1,137 +1,137 @@ -""" -Image-plane point-source solver for strong gravitational lensing. - -Finding the multiple images of a point source requires solving the lens equation -θ = β + α(θ) for θ given a fixed source-plane position β. This is an inverse -problem with no analytic solution for general mass distributions. - -``PointSolver`` solves this numerically using a triangle-tiling approach: - -1. The image plane is tiled with triangles. -2. Each triangle is ray-traced to the source plane. -3. Triangles that contain the source-plane coordinate β are refined recursively. -4. The centroids of the final refined triangles give the image-plane positions. - -The output positions array is padded to a fixed size (``MAX_CONTAINING_SIZE``) using the -sentinel value ``inf`` for JAX compatibility — these ``inf`` entries are stripped by -default but can be retained for use inside a ``jax.jit``-traced function. -""" -import logging -import os -from typing import Tuple, Optional - -import numpy as np -import autoarray as aa -from autoarray.structures.triangles.shape import Point - -from autolens.lens.tracer import Tracer -from .shape_solver import AbstractSolver - - -logger = logging.getLogger(__name__) - - -class PointSolver(AbstractSolver): - - def solve( - self, - tracer: Tracer, - source_plane_coordinate: Tuple[float, float], - xp=None, - plane_redshift: Optional[float] = None, - remove_infinities: Optional[bool] = None, - ) -> aa.Grid2DIrregular: - """ - Solve for the image plane coordinates that are traced to the source plane coordinate. - - This is done by tiling the image plane with triangles and checking if the source plane coordinate is contained - within the triangle. The triangles are sub-sampled to increase the resolution with only the triangles that - contain the source plane coordinate and their neighbours being kept. - - The means of the triangles are then filtered to keep only those with an absolute magnification above the - threshold. - - The positions are stored on an array of fixed shape defined by `MAX_CONTAINING_SIZE`. This ensures the - array is static, which is important for JAX compatibility. This array typically has many entries - which use the sentinel value of `inf`, subsequent JAX calculations incorporated. By default, these - sentinel values are removed from the output, for example general use outside of JAX when simulating - strong lenses. - - Parameters - ---------- - tracer - The tracer that traces the image plane coordinates to the source plane. - source_plane_coordinate - The plane coordinate to trace to the image plane, which by default in the source-plane coordinate - but could be a coordinate in another plane is `plane_redshift` is input. - xp - The array module (``numpy`` or ``jax.numpy``) the solve runs in. ``AnalysisPoint`` - passes ``jax.numpy`` when ``use_jax=True`` is set on the analysis. When ``None`` (the - default), falls back to ``self._xp`` — which is ``jnp`` if the solver was constructed - with ``use_jax=True`` and ``np`` otherwise. Pass explicitly to override. - plane_redshift - The redshift of the plane coordinate, which for multi-plane systems may not be the source-plane. - remove_infinities - Whether to strip the ``inf`` sentinel rows from the output. When ``None`` (the default), - defaults to ``True`` on the NumPy path and ``False`` on the JAX path. The JAX path - keeps the padded static shape so the output crosses a ``jax.jit`` boundary cleanly; - strip the infinities outside the jit if needed. - - Returns - ------- - A ``Grid2DIrregular`` of image-plane coordinates. NumPy-backed on the default path, - ``jax.Array``-backed when ``use_jax=True`` (or ``xp=jnp``). - - Notes - ----- - Smoke-test short-circuit (``PYAUTO_SMALL_DATASETS``): the triangle-tiling solve - is the dominant cost in many simulator scripts and is meaningless on the - downsized grids used for fast smoke tests. When ``PYAUTO_SMALL_DATASETS=1`` is - set the solver returns the fixed pair ``[(1.0, 0.0), (0.0, 1.0)]`` immediately, - skipping ``solve_triangles`` entirely. The two coordinates are well separated - so any downstream ``positions_likelihood_from`` / threshold calculation behaves - normally. ``PYAUTO_SMALL_DATASETS`` is a smoke-test-only flag and is never set - inside a ``jax.jit`` trace, so a plain numpy-backed ``Grid2DIrregular`` is safe - here even when the surrounding analysis uses ``xp=jnp``. - """ - if xp is None: - xp = self._xp - - if remove_infinities is None: - remove_infinities = not self.use_jax - - # NOTE: pytree registration is the user's responsibility (call - # `autolens.jax.register_tracer_classes(tracer)` once before wrapping - # in @jax.jit). Auto-registering inside solve() doesn't help because - # JAX flattens function arguments at trace time — before entering - # this method — so registration must run before the first jitted - # call. See the `lens_calc.py` workspace guide for the canonical - # JIT-it-yourself pattern. - - if os.environ.get("PYAUTO_SMALL_DATASETS") == "1": - return aa.Grid2DIrregular(values=[(1.0, 0.0), (0.0, 1.0)]) - - kept_triangles = super().solve_triangles( - tracer=tracer, - shape=Point(*source_plane_coordinate), - xp=xp, - plane_redshift=plane_redshift, - ) - - filtered_means = self._filter_low_magnification( - tracer=tracer, points=kept_triangles.means, xp=xp - ) - - solution = aa.Grid2DIrregular( - [pair for pair in filtered_means], xp=xp - ).array - - is_nan = xp.isnan(solution).any(axis=1) - sentinel = xp.full_like(solution[0], fill_value=xp.inf) - solution = xp.where(is_nan[:, None], sentinel, solution) - - if remove_infinities: - - solution = solution[~xp.isinf(solution).any(axis=1)] - - return aa.Grid2DIrregular(solution) +""" +Image-plane point-source solver for strong gravitational lensing. + +Finding the multiple images of a point source requires solving the lens equation +θ = β + α(θ) for θ given a fixed source-plane position β. This is an inverse +problem with no analytic solution for general mass distributions. + +``PointSolver`` solves this numerically using a triangle-tiling approach: + +1. The image plane is tiled with triangles. +2. Each triangle is ray-traced to the source plane. +3. Triangles that contain the source-plane coordinate β are refined recursively. +4. The centroids of the final refined triangles give the image-plane positions. + +The output positions array is padded to a fixed size (``MAX_CONTAINING_SIZE``) using the +sentinel value ``inf`` for JAX compatibility — these ``inf`` entries are stripped by +default but can be retained for use inside a ``jax.jit``-traced function. +""" +import logging +import os +from typing import Tuple, Optional + +import numpy as np +import autoarray as aa +from autoarray.structures.triangles.shape import Point + +from autolens.lens.tracer import Tracer +from .shape_solver import AbstractSolver + + +logger = logging.getLogger(__name__) + + +class PointSolver(AbstractSolver): + + def solve( + self, + tracer: Tracer, + source_plane_coordinate: Tuple[float, float], + xp=None, + plane_redshift: Optional[float] = None, + remove_infinities: Optional[bool] = None, + ) -> aa.Grid2DIrregular: + """ + Solve for the image plane coordinates that are traced to the source plane coordinate. + + This is done by tiling the image plane with triangles and checking if the source plane coordinate is contained + within the triangle. The triangles are sub-sampled to increase the resolution with only the triangles that + contain the source plane coordinate and their neighbours being kept. + + The means of the triangles are then filtered to keep only those with an absolute magnification above the + threshold. + + The positions are stored on an array of fixed shape defined by `MAX_CONTAINING_SIZE`. This ensures the + array is static, which is important for JAX compatibility. This array typically has many entries + which use the sentinel value of `inf`, subsequent JAX calculations incorporated. By default, these + sentinel values are removed from the output, for example general use outside of JAX when simulating + strong lenses. + + Parameters + ---------- + tracer + The tracer that traces the image plane coordinates to the source plane. + source_plane_coordinate + The plane coordinate to trace to the image plane, which by default in the source-plane coordinate + but could be a coordinate in another plane is `plane_redshift` is input. + xp + The array module (``numpy`` or ``jax.numpy``) the solve runs in. ``AnalysisPoint`` + passes ``jax.numpy`` when ``use_jax=True`` is set on the analysis. When ``None`` (the + default), falls back to ``self._xp`` — which is ``jnp`` if the solver was constructed + with ``use_jax=True`` and ``np`` otherwise. Pass explicitly to override. + plane_redshift + The redshift of the plane coordinate, which for multi-plane systems may not be the source-plane. + remove_infinities + Whether to strip the ``inf`` sentinel rows from the output. When ``None`` (the default), + defaults to ``True`` on the NumPy path and ``False`` on the JAX path. The JAX path + keeps the padded static shape so the output crosses a ``jax.jit`` boundary cleanly; + strip the infinities outside the jit if needed. + + Returns + ------- + A ``Grid2DIrregular`` of image-plane coordinates. NumPy-backed on the default path, + ``jax.Array``-backed when ``use_jax=True`` (or ``xp=jnp``). + + Notes + ----- + Smoke-test short-circuit (``PYAUTO_SMALL_DATASETS``): the triangle-tiling solve + is the dominant cost in many simulator scripts and is meaningless on the + downsized grids used for fast smoke tests. When ``PYAUTO_SMALL_DATASETS=1`` is + set the solver returns the fixed pair ``[(1.0, 0.0), (0.0, 1.0)]`` immediately, + skipping ``solve_triangles`` entirely. The two coordinates are well separated + so any downstream ``positions_likelihood_from`` / threshold calculation behaves + normally. ``PYAUTO_SMALL_DATASETS`` is a smoke-test-only flag and is never set + inside a ``jax.jit`` trace, so a plain numpy-backed ``Grid2DIrregular`` is safe + here even when the surrounding analysis uses ``xp=jnp``. + """ + if xp is None: + xp = self._xp + + if remove_infinities is None: + remove_infinities = not self.use_jax + + # NOTE: pytree registration is the user's responsibility (call + # `autolens.jax.register_tracer_classes(tracer)` once before wrapping + # in @jax.jit). Auto-registering inside solve() doesn't help because + # JAX flattens function arguments at trace time — before entering + # this method — so registration must run before the first jitted + # call. See the `lens_calc.py` workspace guide for the canonical + # JIT-it-yourself pattern. + + if os.environ.get("PYAUTO_SMALL_DATASETS") == "1": + return aa.Grid2DIrregular(values=[(1.0, 0.0), (0.0, 1.0)]) + + kept_triangles = super().solve_triangles( + tracer=tracer, + shape=Point(*source_plane_coordinate), + xp=xp, + plane_redshift=plane_redshift, + ) + + filtered_means = self._filter_low_magnification( + tracer=tracer, points=kept_triangles.means, xp=xp + ) + + solution = aa.Grid2DIrregular( + [pair for pair in filtered_means], xp=xp + ).array + + is_nan = xp.isnan(solution).any(axis=1) + sentinel = xp.full_like(solution[0], fill_value=xp.inf) + solution = xp.where(is_nan[:, None], sentinel, solution) + + if remove_infinities: + + solution = solution[~xp.isinf(solution).any(axis=1)] + + return aa.Grid2DIrregular(solution) diff --git a/autolens/util/__init__.py b/autolens/util/__init__.py index 10cc4fc32..2d6e9e33a 100644 --- a/autolens/util/__init__.py +++ b/autolens/util/__init__.py @@ -1,25 +1,25 @@ -from autoarray.geometry import geometry_util as geometry -from autoarray.mask import mask_1d_util as mask_1d -from autoarray.mask import mask_2d_util as mask_2d -from autoarray.operators.over_sampling import over_sample_util as over_sample -from autoarray.structures.arrays import array_1d_util as array_1d -from autoarray.structures.arrays import array_2d_util as array_2d -from autoarray.structures.grids import grid_1d_util as grid_1d -from autoarray.structures.grids import grid_2d_util as grid_2d -from autoarray.structures.grids import sparse_2d_util as sparse -from autoarray.fit import fit_util as fit -from autoarray.inversion.mappers import mapper_util as mapper -from autoarray.inversion.regularization import regularization_util as regularization -from autoarray.inversion.inversion import inversion_util as inversion -from autoarray.inversion.inversion.imaging import ( - inversion_imaging_util as inversion_imaging, -) -from autoarray.inversion.inversion.interferometer import ( - inversion_interferometer_util as inversion_interferometer, -) -from autoarray.operators import transformer_util as transformer -from autoarray.util import dataset_util as dataset -from autogalaxy.util import error_util as error -from autogalaxy.analysis import chaining_util as chaining - -from autolens.lens import tracer_util as tracer +from autoarray.geometry import geometry_util as geometry +from autoarray.mask import mask_1d_util as mask_1d +from autoarray.mask import mask_2d_util as mask_2d +from autoarray.operators.over_sampling import over_sample_util as over_sample +from autoarray.structures.arrays import array_1d_util as array_1d +from autoarray.structures.arrays import array_2d_util as array_2d +from autoarray.structures.grids import grid_1d_util as grid_1d +from autoarray.structures.grids import grid_2d_util as grid_2d +from autoarray.structures.grids import sparse_2d_util as sparse +from autoarray.fit import fit_util as fit +from autoarray.inversion.mappers import mapper_util as mapper +from autoarray.inversion.regularization import regularization_util as regularization +from autoarray.inversion.inversion import inversion_util as inversion +from autoarray.inversion.inversion.imaging import ( + inversion_imaging_util as inversion_imaging, +) +from autoarray.inversion.inversion.interferometer import ( + inversion_interferometer_util as inversion_interferometer, +) +from autoarray.operators import transformer_util as transformer +from autoarray.util import dataset_util as dataset +from autogalaxy.util import error_util as error +from autogalaxy.analysis import chaining_util as chaining + +from autolens.lens import tracer_util as tracer diff --git a/docs/_templates/custom-class-template.rst b/docs/_templates/custom-class-template.rst index 14b3b95f3..e6acd3c5b 100644 --- a/docs/_templates/custom-class-template.rst +++ b/docs/_templates/custom-class-template.rst @@ -1,36 +1,36 @@ -{{ fullname | escape | underline}} - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - :members: - :show-inheritance: - :exclude-members: ndarray, __init__, __new__ - :special-members: __call__, __add__, __mul__ - - {% block methods %} - {% if methods %} - .. rubric:: {{ _('Methods') }} - - .. autosummary:: - :nosignatures: - {% for item in methods %} - {%- if not item.startswith('_') %} - ~{{ name }}.{{ item }} - {%- endif -%} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block attributes %} - {% if attributes %} - .. rubric:: {{ _('Attributes') }} - - .. autosummary:: - {% for item in attributes %} - {%- if not item.startswith('_') %} - ~{{ name }}.{{ item }} - {%- endif -%} - {%- endfor %} - {% endif %} +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :show-inheritance: + :exclude-members: ndarray, __init__, __new__ + :special-members: __call__, __add__, __mul__ + + {% block methods %} + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + :nosignatures: + {% for item in methods %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} {% endblock %} \ No newline at end of file diff --git a/docs/_templates/custom_module_template.rst b/docs/_templates/custom_module_template.rst index fd46b9053..dd90e32ec 100644 --- a/docs/_templates/custom_module_template.rst +++ b/docs/_templates/custom_module_template.rst @@ -1,66 +1,66 @@ -{{ fullname | escape | underline}} - -.. automodule:: {{ fullname }} - - {% block attributes %} - {% if attributes %} - .. rubric:: Module attributes - - .. autosummary:: - :toctree: - {% for item in attributes %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block functions %} - {% if functions %} - .. rubric:: {{ _('Functions') }} - - .. autosummary:: - :toctree: - :nosignatures: - {% for item in functions %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block classes %} - {% if classes %} - .. rubric:: {{ _('Classes') }} - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - {% for item in classes %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block exceptions %} - {% if exceptions %} - .. rubric:: {{ _('Exceptions') }} - - .. autosummary:: - :toctree: - {% for item in exceptions %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - -{% block modules %} -{% if modules %} -.. autosummary:: - :toctree: - :template: custom-module-template.rst - :recursive: -{% for item in modules %} - {{ item }} -{%- endfor %} -{% endif %} +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :template: custom-class-template.rst + :nosignatures: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} {% endblock %} \ No newline at end of file diff --git a/docs/api/data.rst b/docs/api/data.rst index 65df022aa..13d7caeed 100644 --- a/docs/api/data.rst +++ b/docs/api/data.rst @@ -1,89 +1,89 @@ -=============== -Data Structures -=============== - - -2D Data Structures ------------------- - -Two-dimensional data structures store and mask 2D arrays containing data (e.g. images) and -grids of (y,x) Cartesian coordinates (which are used for evaluating light profiles). - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Mask2D - Array2D - Grid2D - Grid2DIrregular - -Imaging -------- - -For datasets taken with a CCD (or similar imaging device), including objects which perform -2D convolution. - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Imaging - SimulatorImaging - Convolver - - -Interferometer --------------- - -For datasets taken with an interferometer (E.g. ALMA), including objects which perform -a fast Fourier transform to map data to the uv-plane. - -.. autosummary:: - :toctree: _autosummary - - Interferometer - SimulatorInterferometer - Visibilities - TransformerDFT - TransformerNUFFT - -Over Sampling -------------- - -Calculations using grids approximate a 2D line integral of the light in the galaxy which falls in each image-pixel. -Different over sampling schemes can be used to efficiently approximate this integral and these objects can be -applied to datasets to apply over sampling to their fit. - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - OverSampler - - -1D Data Structures ------------------- - -One-dimensional data structures store and mask 1D arrays and grids of (x) Cartesian -coordinates. - -Their most common use is manipulating 1D representations of a light or mass -profile (e.g. computing the intensity versus radius in 1D, or convergene vs radius). - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Mask1D - Array1D - ArrayIrregular +=============== +Data Structures +=============== + + +2D Data Structures +------------------ + +Two-dimensional data structures store and mask 2D arrays containing data (e.g. images) and +grids of (y,x) Cartesian coordinates (which are used for evaluating light profiles). + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Mask2D + Array2D + Grid2D + Grid2DIrregular + +Imaging +------- + +For datasets taken with a CCD (or similar imaging device), including objects which perform +2D convolution. + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Imaging + SimulatorImaging + Convolver + + +Interferometer +-------------- + +For datasets taken with an interferometer (E.g. ALMA), including objects which perform +a fast Fourier transform to map data to the uv-plane. + +.. autosummary:: + :toctree: _autosummary + + Interferometer + SimulatorInterferometer + Visibilities + TransformerDFT + TransformerNUFFT + +Over Sampling +------------- + +Calculations using grids approximate a 2D line integral of the light in the galaxy which falls in each image-pixel. +Different over sampling schemes can be used to efficiently approximate this integral and these objects can be +applied to datasets to apply over sampling to their fit. + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + OverSampler + + +1D Data Structures +------------------ + +One-dimensional data structures store and mask 1D arrays and grids of (x) Cartesian +coordinates. + +Their most common use is manipulating 1D representations of a light or mass +profile (e.g. computing the intensity versus radius in 1D, or convergene vs radius). + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Mask1D + Array1D + ArrayIrregular Grid1D \ No newline at end of file diff --git a/docs/api/fitting.rst b/docs/api/fitting.rst index 886ac92a7..b15195a75 100644 --- a/docs/api/fitting.rst +++ b/docs/api/fitting.rst @@ -1,19 +1,19 @@ -======= -Fitting -======= - -Imaging and Interferometer --------------------------- - -For fitting a lens model (composed from ``Galaxy`` objects in a ``Tracer``) to an -imaging or interferometer dataset. - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - FitImaging +======= +Fitting +======= + +Imaging and Interferometer +-------------------------- + +For fitting a lens model (composed from ``Galaxy`` objects in a ``Tracer``) to an +imaging or interferometer dataset. + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + FitImaging FitInterferometer \ No newline at end of file diff --git a/docs/api/galaxy.rst b/docs/api/galaxy.rst index 2aaffe1d4..311be2180 100644 --- a/docs/api/galaxy.rst +++ b/docs/api/galaxy.rst @@ -1,36 +1,36 @@ -=============== -Galaxy / Tracer -=============== - -Galaxy / Tracer ---------------- - -``Galaxy`` and ``Galaxies`` model individual galaxies with light and mass profiles at a -given redshift. ``Tracer`` groups galaxies by redshift into planes and performs -multi-plane gravitational lensing ray-tracing, computing lensed images, convergence, -deflection angles, magnification, critical curves, and caustics. - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Galaxy - Galaxies - Tracer - -To treat the redshift of a galaxy as a free parameter in a model, the ``Redshift`` object must -be used. - -This is because **PyAutoFit** (which handles model-fitting), requires all parameters to be a Python class. - -The ``Redshift`` object does not need to be used for general **PyAutoGalaxy** use. - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - +=============== +Galaxy / Tracer +=============== + +Galaxy / Tracer +--------------- + +``Galaxy`` and ``Galaxies`` model individual galaxies with light and mass profiles at a +given redshift. ``Tracer`` groups galaxies by redshift into planes and performs +multi-plane gravitational lensing ray-tracing, computing lensed images, convergence, +deflection angles, magnification, critical curves, and caustics. + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Galaxy + Galaxies + Tracer + +To treat the redshift of a galaxy as a free parameter in a model, the ``Redshift`` object must +be used. + +This is because **PyAutoFit** (which handles model-fitting), requires all parameters to be a Python class. + +The ``Redshift`` object does not need to be used for general **PyAutoGalaxy** use. + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + Redshift \ No newline at end of file diff --git a/docs/api/light.rst b/docs/api/light.rst index 6069b7451..8226baea6 100644 --- a/docs/api/light.rst +++ b/docs/api/light.rst @@ -1,99 +1,99 @@ -============== -Light Profiles -============== - -Standard [``ag.lp``] --------------------- - -Standard parametric light profiles whose ``intensity`` is a free parameter of the model. - -.. currentmodule:: autogalaxy.profiles.light.standard - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Gaussian - GaussianSph - GaussianMultipole - Sersic - SersicSph - SersicMultipole - Exponential - ExponentialSph - DevVaucouleurs - DevVaucouleursSph - SersicCore - SersicCoreSph - ExponentialCore - ExponentialCoreSph - Chameleon - ChameleonSph - ElsonFreeFall - ElsonFreeFallSph - -Linear [``ag.lp_linear``] --------------------------- - -Linear light profiles whose ``intensity`` is not a free parameter but is instead solved -analytically via a linear matrix inversion during each likelihood evaluation. This allows -many profiles to be combined efficiently without exploding the non-linear parameter space. - -.. currentmodule:: autogalaxy.profiles.light.linear - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Gaussian - GaussianSph - GaussianMultipole - Sersic - SersicSph - SersicMultipole - Exponential - ExponentialSph - DevVaucouleurs - DevVaucouleursSph - SersicCore - SersicCoreSph - ExponentialCore - ExponentialCoreSph - -Operated [``ag.lp_operated``] ------------------------------- - -Operated light profiles that represent emission which has already had an instrument -operation (e.g. PSF convolution) applied to it. The ``operated_only`` parameter on -fitting classes controls whether these profiles are included or excluded from a given -image computation. - -.. currentmodule:: autogalaxy.profiles.light.operated - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Gaussian - Moffat - Sersic - -Basis [``ag.lp_basis``] ------------------------- - -A ``Basis`` groups a collection of light profiles (e.g. a Multi-Gaussian Expansion or -shapelet decomposition) so that they behave as a single profile in the model. When the -constituent profiles are ``LightProfileLinear`` instances their intensities are all solved -simultaneously via a single inversion. - -.. currentmodule:: autogalaxy.profiles.basis - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - +============== +Light Profiles +============== + +Standard [``ag.lp``] +-------------------- + +Standard parametric light profiles whose ``intensity`` is a free parameter of the model. + +.. currentmodule:: autogalaxy.profiles.light.standard + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Gaussian + GaussianSph + GaussianMultipole + Sersic + SersicSph + SersicMultipole + Exponential + ExponentialSph + DevVaucouleurs + DevVaucouleursSph + SersicCore + SersicCoreSph + ExponentialCore + ExponentialCoreSph + Chameleon + ChameleonSph + ElsonFreeFall + ElsonFreeFallSph + +Linear [``ag.lp_linear``] +-------------------------- + +Linear light profiles whose ``intensity`` is not a free parameter but is instead solved +analytically via a linear matrix inversion during each likelihood evaluation. This allows +many profiles to be combined efficiently without exploding the non-linear parameter space. + +.. currentmodule:: autogalaxy.profiles.light.linear + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Gaussian + GaussianSph + GaussianMultipole + Sersic + SersicSph + SersicMultipole + Exponential + ExponentialSph + DevVaucouleurs + DevVaucouleursSph + SersicCore + SersicCoreSph + ExponentialCore + ExponentialCoreSph + +Operated [``ag.lp_operated``] +------------------------------ + +Operated light profiles that represent emission which has already had an instrument +operation (e.g. PSF convolution) applied to it. The ``operated_only`` parameter on +fitting classes controls whether these profiles are included or excluded from a given +image computation. + +.. currentmodule:: autogalaxy.profiles.light.operated + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Gaussian + Moffat + Sersic + +Basis [``ag.lp_basis``] +------------------------ + +A ``Basis`` groups a collection of light profiles (e.g. a Multi-Gaussian Expansion or +shapelet decomposition) so that they behave as a single profile in the model. When the +constituent profiles are ``LightProfileLinear`` instances their intensities are all solved +simultaneously via a single inversion. + +.. currentmodule:: autogalaxy.profiles.basis + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + Basis \ No newline at end of file diff --git a/docs/api/modeling.rst b/docs/api/modeling.rst index 915ea5c13..b5b8bcc1d 100644 --- a/docs/api/modeling.rst +++ b/docs/api/modeling.rst @@ -1,66 +1,66 @@ -============= -Lens Modeling -============= - -Analysis -======== - -The ``Analysis`` objects define the ``log_likelihood_function`` of how a lens model is fitted to a dataset. - -It acts as an interface between the data, model and the non-linear search. - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - AnalysisImaging - AnalysisInterferometer - -Non-linear Searches -------------------- - -A non-linear search is an algorithm which fits a model to data. - -**PyAutoGalaxy** currently supports three types of non-linear search algorithms: nested samplers, -Markov Chain Monte Carlo (MCMC) and Maximum Likelihood Estimaotrs (MLE). - -.. currentmodule:: autofit - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Nautilus - LBFGS - BFGS - DynestyDynamic - Emcee - -Priors ------- - -The priors of parameters of every component of a model, which is fitted to data, are customized using ``Prior`` objects. - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - UniformPrior - GaussianPrior - LogUniformPrior - LogGaussianPrior - -Adapt ------ - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: generated/ - +============= +Lens Modeling +============= + +Analysis +======== + +The ``Analysis`` objects define the ``log_likelihood_function`` of how a lens model is fitted to a dataset. + +It acts as an interface between the data, model and the non-linear search. + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + AnalysisImaging + AnalysisInterferometer + +Non-linear Searches +------------------- + +A non-linear search is an algorithm which fits a model to data. + +**PyAutoGalaxy** currently supports three types of non-linear search algorithms: nested samplers, +Markov Chain Monte Carlo (MCMC) and Maximum Likelihood Estimaotrs (MLE). + +.. currentmodule:: autofit + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Nautilus + LBFGS + BFGS + DynestyDynamic + Emcee + +Priors +------ + +The priors of parameters of every component of a model, which is fitted to data, are customized using ``Prior`` objects. + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + UniformPrior + GaussianPrior + LogUniformPrior + LogGaussianPrior + +Adapt +----- + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: generated/ + AdaptImages \ No newline at end of file diff --git a/docs/api/pixelization.rst b/docs/api/pixelization.rst index 9c78143ef..2e221603b 100644 --- a/docs/api/pixelization.rst +++ b/docs/api/pixelization.rst @@ -1,89 +1,89 @@ -============= -Pixelizations -============= - -Pixelization ------------- - -Groups all of the individual components used to reconstruct a galaxy via a -pixelization (an ``ImageMesh``, ``Mesh`` and ``Regularization``) - -The ``Pixelization`` API documentation provides a comprehensive description of how pixelizaiton objects work and -their associated API. - -**It is recommended you read this documentation before using pixelizations**. - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Pixelization - -Image Mesh [ag.image_mesh] --------------------------- - -.. currentmodule:: autoarray.inversion.mesh.image_mesh - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Overlay - Hilbert - KMeans - -Mesh [ag.mesh] --------------- - -.. currentmodule:: autoarray.inversion.mesh.mesh - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - RectangularAdaptDensity - Delaunay - -Regularization [ag.reg] ------------------------ - -.. currentmodule:: autoarray.inversion.regularization - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Constant - ConstantSplit - Adapt - AdaptSplit - -Settings --------- - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - Settings - -Mapper ------- - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - +============= +Pixelizations +============= + +Pixelization +------------ + +Groups all of the individual components used to reconstruct a galaxy via a +pixelization (an ``ImageMesh``, ``Mesh`` and ``Regularization``) + +The ``Pixelization`` API documentation provides a comprehensive description of how pixelizaiton objects work and +their associated API. + +**It is recommended you read this documentation before using pixelizations**. + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Pixelization + +Image Mesh [ag.image_mesh] +-------------------------- + +.. currentmodule:: autoarray.inversion.mesh.image_mesh + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Overlay + Hilbert + KMeans + +Mesh [ag.mesh] +-------------- + +.. currentmodule:: autoarray.inversion.mesh.mesh + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + RectangularAdaptDensity + Delaunay + +Regularization [ag.reg] +----------------------- + +.. currentmodule:: autoarray.inversion.regularization + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Constant + ConstantSplit + Adapt + AdaptSplit + +Settings +-------- + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + Settings + +Mapper +------ + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + Mapper \ No newline at end of file diff --git a/docs/api/plot.rst b/docs/api/plot.rst index 8edfe538c..0a27e2819 100644 --- a/docs/api/plot.rst +++ b/docs/api/plot.rst @@ -1,148 +1,148 @@ -======== -Plotting -======== - -**PyAutoLens** custom visualization library. - -Step-by-step Juypter notebook guides illustrating all objects listed on this page are -provided on the `autolens_workspace: plot tutorials `_ and -it is strongly recommended you use those to learn plot customization. - -**Examples / Tutorials:** - -- `autolens_workspace: plot tutorials `_ - -Plotters [aplt] ---------------- - -Create figures and subplots showing quantities of standard **PyAutoLens** objects. - -.. currentmodule:: autolens.plot - -**Basic Plot Functions:** - -.. autosummary:: - :toctree: _autosummary - - plot_array - plot_grid - -**Tracer and Galaxies Subplots:** - -.. autosummary:: - :toctree: _autosummary - - subplot_tracer - subplot_lensed_images - subplot_galaxies_images - -**Imaging Fit Subplots:** - -.. autosummary:: - :toctree: _autosummary - - subplot_fit_imaging - subplot_fit_imaging_log10 - subplot_fit_imaging_x1_plane - subplot_fit_imaging_log10_x1_plane - subplot_fit_imaging_of_planes - subplot_fit_imaging_tracer - subplot_fit_combined - subplot_fit_combined_log10 - -**Interferometer Fit Subplots:** - -.. autosummary:: - :toctree: _autosummary - - subplot_fit_interferometer - subplot_fit_interferometer_real_space - -**Point Source Subplots:** - -.. autosummary:: - :toctree: _autosummary - - subplot_fit_point - subplot_point_dataset - -**Subhalo Detection Subplots:** - -.. autosummary:: - :toctree: _autosummary - - subplot_detection_imaging - subplot_detection_fits - -**Sensitivity Mapping Subplots:** - -.. autosummary:: - :toctree: _autosummary - - subplot_sensitivity_tracer_images - subplot_sensitivity - subplot_sensitivity_figures_of_merit - -Non-linear Search Plot Functions [aplt] ---------------------------------------- - -Module-level functions for visualizing non-linear search results. - -.. currentmodule:: autofit.plot - -.. autosummary:: - :toctree: _autosummary - - corner_cornerpy - corner_anesthetic - subplot_parameters - log_likelihood_vs_iteration - -Plot Customization [aplt] -------------------------- - -The plotting API is **functional**: customization is done by passing keyword -arguments directly to any ``aplt`` plotting function. There is no ``MatPlot2D`` -object anymore (nor the old ``Cmap`` / ``Visuals`` / ``Units`` / matplotlib-wrapper -classes) — these were removed in favour of the keyword-argument interface below. - -Every figure and subplot function accepts: - -- ``title`` — the figure title. -- ``colormap`` — the matplotlib colormap name (e.g. ``"jet"``, ``"hot"``, ``"gray"``). -- ``use_log10`` — if ``True``, plot the colormap on a ``log10`` scale. -- ``output_path`` — directory to save the figure to (omit to display it interactively). -- ``output_filename`` — the saved file's name. -- ``output_format`` — the saved file's format, e.g. ``"png"`` or ``"pdf"``. - -For example: - -.. code-block:: python - - import autolens.plot as aplt - - # Customize the appearance: - aplt.plot_array(array=image, title="Image", colormap="jet", use_log10=True) - - # Save to disk instead of displaying: - aplt.plot_array( - array=image, output_path="output", output_filename="image", output_format="png" - ) - -Default plotting values (figure size, fonts, colormaps, ticks, labels, ...) are set -via the workspace ``config/visualize`` YAML files rather than in code, so most figures -need no customization at all. - -Figure Output [aplt] --------------------- - -The ``Output`` object gives lower-level control of how and where figures are written -to disk, and is accepted by the plotting functions in place of the individual -``output_*`` keyword arguments. - -.. currentmodule:: autoarray.plot - -.. autosummary:: - :toctree: _autosummary - - Output +======== +Plotting +======== + +**PyAutoLens** custom visualization library. + +Step-by-step Juypter notebook guides illustrating all objects listed on this page are +provided on the `autolens_workspace: plot tutorials `_ and +it is strongly recommended you use those to learn plot customization. + +**Examples / Tutorials:** + +- `autolens_workspace: plot tutorials `_ + +Plotters [aplt] +--------------- + +Create figures and subplots showing quantities of standard **PyAutoLens** objects. + +.. currentmodule:: autolens.plot + +**Basic Plot Functions:** + +.. autosummary:: + :toctree: _autosummary + + plot_array + plot_grid + +**Tracer and Galaxies Subplots:** + +.. autosummary:: + :toctree: _autosummary + + subplot_tracer + subplot_lensed_images + subplot_galaxies_images + +**Imaging Fit Subplots:** + +.. autosummary:: + :toctree: _autosummary + + subplot_fit_imaging + subplot_fit_imaging_log10 + subplot_fit_imaging_x1_plane + subplot_fit_imaging_log10_x1_plane + subplot_fit_imaging_of_planes + subplot_fit_imaging_tracer + subplot_fit_combined + subplot_fit_combined_log10 + +**Interferometer Fit Subplots:** + +.. autosummary:: + :toctree: _autosummary + + subplot_fit_interferometer + subplot_fit_interferometer_real_space + +**Point Source Subplots:** + +.. autosummary:: + :toctree: _autosummary + + subplot_fit_point + subplot_point_dataset + +**Subhalo Detection Subplots:** + +.. autosummary:: + :toctree: _autosummary + + subplot_detection_imaging + subplot_detection_fits + +**Sensitivity Mapping Subplots:** + +.. autosummary:: + :toctree: _autosummary + + subplot_sensitivity_tracer_images + subplot_sensitivity + subplot_sensitivity_figures_of_merit + +Non-linear Search Plot Functions [aplt] +--------------------------------------- + +Module-level functions for visualizing non-linear search results. + +.. currentmodule:: autofit.plot + +.. autosummary:: + :toctree: _autosummary + + corner_cornerpy + corner_anesthetic + subplot_parameters + log_likelihood_vs_iteration + +Plot Customization [aplt] +------------------------- + +The plotting API is **functional**: customization is done by passing keyword +arguments directly to any ``aplt`` plotting function. There is no ``MatPlot2D`` +object anymore (nor the old ``Cmap`` / ``Visuals`` / ``Units`` / matplotlib-wrapper +classes) — these were removed in favour of the keyword-argument interface below. + +Every figure and subplot function accepts: + +- ``title`` — the figure title. +- ``colormap`` — the matplotlib colormap name (e.g. ``"jet"``, ``"hot"``, ``"gray"``). +- ``use_log10`` — if ``True``, plot the colormap on a ``log10`` scale. +- ``output_path`` — directory to save the figure to (omit to display it interactively). +- ``output_filename`` — the saved file's name. +- ``output_format`` — the saved file's format, e.g. ``"png"`` or ``"pdf"``. + +For example: + +.. code-block:: python + + import autolens.plot as aplt + + # Customize the appearance: + aplt.plot_array(array=image, title="Image", colormap="jet", use_log10=True) + + # Save to disk instead of displaying: + aplt.plot_array( + array=image, output_path="output", output_filename="image", output_format="png" + ) + +Default plotting values (figure size, fonts, colormaps, ticks, labels, ...) are set +via the workspace ``config/visualize`` YAML files rather than in code, so most figures +need no customization at all. + +Figure Output [aplt] +-------------------- + +The ``Output`` object gives lower-level control of how and where figures are written +to disk, and is accepted by the plotting functions in place of the individual +``output_*`` keyword arguments. + +.. currentmodule:: autoarray.plot + +.. autosummary:: + :toctree: _autosummary + + Output diff --git a/docs/api/point.rst b/docs/api/point.rst index 9bc7ab7a2..f53ab3b67 100644 --- a/docs/api/point.rst +++ b/docs/api/point.rst @@ -1,79 +1,79 @@ -============= -Point Sources -============= - -Point sources arise when the background object is compact (e.g. a quasar, supernova, or -compact radio source) and is modelled by its image-plane positions, flux ratios, and/or -time delays rather than by a resolved surface-brightness distribution. - -Dataset -------- - -Data structures holding the observed positions, fluxes, and time delays of one or more -named point sources. - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - PointDataset - -Solver ------- - -The ``PointSolver`` finds the image-plane positions that correspond to a given -source-plane coordinate by solving the lens equation numerically via a triangle-tiling -approach. - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - PointSolver - -Fitting -------- - -Fit classes for point-source positions, flux ratios, and time delays. ``FitPointDataset`` -orchestrates all active components; the individual fit classes can also be used standalone. - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - FitPointDataset - FitPositionsImagePair - FitPositionsImagePairAll - FitPositionsImagePairRepeat - FitPositionsSource - FitFluxes - FitTimeDelays - -Position Likelihood -------------------- - -``PositionsLH`` adds a penalty to the log likelihood when the observed image positions do -not self-consistently trace back to the same source-plane location, guiding the non-linear -search toward physically consistent mass models. - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - PositionsLH - -Analysis --------- - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - +============= +Point Sources +============= + +Point sources arise when the background object is compact (e.g. a quasar, supernova, or +compact radio source) and is modelled by its image-plane positions, flux ratios, and/or +time delays rather than by a resolved surface-brightness distribution. + +Dataset +------- + +Data structures holding the observed positions, fluxes, and time delays of one or more +named point sources. + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + PointDataset + +Solver +------ + +The ``PointSolver`` finds the image-plane positions that correspond to a given +source-plane coordinate by solving the lens equation numerically via a triangle-tiling +approach. + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + PointSolver + +Fitting +------- + +Fit classes for point-source positions, flux ratios, and time delays. ``FitPointDataset`` +orchestrates all active components; the individual fit classes can also be used standalone. + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + FitPointDataset + FitPositionsImagePair + FitPositionsImagePairAll + FitPositionsImagePairRepeat + FitPositionsSource + FitFluxes + FitTimeDelays + +Position Likelihood +------------------- + +``PositionsLH`` adds a penalty to the log likelihood when the observed image positions do +not self-consistently trace back to the same source-plane location, guiding the non-linear +search toward physically consistent mass models. + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + PositionsLH + +Analysis +-------- + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + AnalysisPoint \ No newline at end of file diff --git a/docs/api/source.rst b/docs/api/source.rst index 0e75be86e..4bd8d1367 100644 --- a/docs/api/source.rst +++ b/docs/api/source.rst @@ -1,34 +1,34 @@ -=========== -Source Code -=========== - -This page provided API docs for functionality which is typically not used by users, but is used internally in the -**PyAutoGalaxy** source code. - -These docs are intended for developers, or users doing non-standard computations using internal **PyAutoFit** objects. - -Geometry Profiles ------------------ - -.. currentmodule:: autogalaxy.profiles.geometry_profiles - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - EllProfile - SphProfile - -Operators ---------- - -.. currentmodule:: autolens - -.. autosummary:: - :toctree: _autosummary - :template: custom-class-template.rst - :recursive: - - OperateImage +=========== +Source Code +=========== + +This page provided API docs for functionality which is typically not used by users, but is used internally in the +**PyAutoGalaxy** source code. + +These docs are intended for developers, or users doing non-standard computations using internal **PyAutoFit** objects. + +Geometry Profiles +----------------- + +.. currentmodule:: autogalaxy.profiles.geometry_profiles + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + EllProfile + SphProfile + +Operators +--------- + +.. currentmodule:: autolens + +.. autosummary:: + :toctree: _autosummary + :template: custom-class-template.rst + :recursive: + + OperateImage LensCalc \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index ea4e8def2..eea8b8593 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,147 +1,147 @@ -import datetime - -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use Path(...).resolve() to make it absolute, like shown here. -# - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(".").resolve())) - -import autolens - -# -- Project information ----------------------------------------------------- - -year = datetime.date.today().year -project = "PyAutoLens" -copyright = "2025, James Nightingale, Richard Hayes" -author = "James Nightingale, Richard Hayes" - -# The full version, including alpha/beta/rc tags -release = autolens.__version__ -master_doc = "index" - - -# -- General configuration --------------------------------------------------- - -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.extlinks", - "sphinx.ext.intersphinx", - "sphinx.ext.mathjax", - "sphinx.ext.todo", - "sphinx.ext.viewcode", - "sphinx.ext.napoleon", - "numpydoc", - "sphinx_autodoc_typehints", - # external - "myst_parser", - "sphinx_copybutton", - "sphinx_design", - "sphinx_inline_tabs", -] - -set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints -add_module_names = False # Remove namespaces from class/method signatures - -templates_path = ["_templates"] - -# -- Options for extlinks ---------------------------------------------------- - -extlinks = {"pypi": ("https://pypi.org/project/%s/", "")} - -# -- Options for intersphinx ------------------------------------------------- - -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "sphinx": ("https://www.sphinx-doc.org/en/master", None), -} - -# -- Options for TODOs ------------------------------------------------------- - -todo_include_todos = True - -# -- Options for Markdown files ---------------------------------------------- - -myst_enable_extensions = ["colon_fence", "deflist"] -myst_heading_anchors = 3 - -autosummary_generate = True -autosummary_imported_members = True -autodoc_member_order = "bysource" -autodoc_default_options = { - "members": True, - "undoc-members": True, - "show-inheritance": True, -} -autodoc_class_signature = "separated" -autoclass_content = "init" - -numpydoc_show_class_members = False -numpydoc_show_inherited_class_members = False -numpydoc_class_members_toctree = True - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [ - "_build", - "Thumbs.db", - ".DS_Store", - "CODE_OF_CONDUCT.md", - "CONTRIBUTING.md", - "CITATIONS.md", - "README.md", -] - - -# -- Options for HTML output ------------------------------------------------- - -html_theme = "furo" -html_title = "PyAutoLens" -html_short_title = "PyAutoLens" -html_permalinks_icon = "#" -html_last_updated_fmt = "%b %d, %Y" - -html_show_sourcelink = False -html_show_sphinx = True -html_show_copyright = True - -pygments_style = "sphinx" -pygments_dark_style = "monokai" -add_function_parentheses = False - -language = "en" - -html_static_path = ["_static"] -html_css_files = ["pyauto.css"] - -html_theme_options = { - "light_css_variables": { - "color-brand-primary": "#7c4dff", - "color-brand-content": "#7c4dff", - }, - "dark_css_variables": { - "color-brand-primary": "#9d7aff", - "color-brand-content": "#9d7aff", - }, -} - -from sphinx.builders.html import StandaloneHTMLBuilder - -StandaloneHTMLBuilder.supported_image_types = ["image/gif", "image/png", "image/jpeg"] - -typehints_fully_qualified = False -always_document_param_types = False -typehints_document_rtype = False +import datetime + +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use Path(...).resolve() to make it absolute, like shown here. +# + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(".").resolve())) + +import autolens + +# -- Project information ----------------------------------------------------- + +year = datetime.date.today().year +project = "PyAutoLens" +copyright = "2025, James Nightingale, Richard Hayes" +author = "James Nightingale, Richard Hayes" + +# The full version, including alpha/beta/rc tags +release = autolens.__version__ +master_doc = "index" + + +# -- General configuration --------------------------------------------------- + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "numpydoc", + "sphinx_autodoc_typehints", + # external + "myst_parser", + "sphinx_copybutton", + "sphinx_design", + "sphinx_inline_tabs", +] + +set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints +add_module_names = False # Remove namespaces from class/method signatures + +templates_path = ["_templates"] + +# -- Options for extlinks ---------------------------------------------------- + +extlinks = {"pypi": ("https://pypi.org/project/%s/", "")} + +# -- Options for intersphinx ------------------------------------------------- + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "sphinx": ("https://www.sphinx-doc.org/en/master", None), +} + +# -- Options for TODOs ------------------------------------------------------- + +todo_include_todos = True + +# -- Options for Markdown files ---------------------------------------------- + +myst_enable_extensions = ["colon_fence", "deflist"] +myst_heading_anchors = 3 + +autosummary_generate = True +autosummary_imported_members = True +autodoc_member_order = "bysource" +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, +} +autodoc_class_signature = "separated" +autoclass_content = "init" + +numpydoc_show_class_members = False +numpydoc_show_inherited_class_members = False +numpydoc_class_members_toctree = True + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "CITATIONS.md", + "README.md", +] + + +# -- Options for HTML output ------------------------------------------------- + +html_theme = "furo" +html_title = "PyAutoLens" +html_short_title = "PyAutoLens" +html_permalinks_icon = "#" +html_last_updated_fmt = "%b %d, %Y" + +html_show_sourcelink = False +html_show_sphinx = True +html_show_copyright = True + +pygments_style = "sphinx" +pygments_dark_style = "monokai" +add_function_parentheses = False + +language = "en" + +html_static_path = ["_static"] +html_css_files = ["pyauto.css"] + +html_theme_options = { + "light_css_variables": { + "color-brand-primary": "#7c4dff", + "color-brand-content": "#7c4dff", + }, + "dark_css_variables": { + "color-brand-primary": "#9d7aff", + "color-brand-content": "#9d7aff", + }, +} + +from sphinx.builders.html import StandaloneHTMLBuilder + +StandaloneHTMLBuilder.supported_image_types = ["image/gif", "image/png", "image/jpeg"] + +typehints_fully_qualified = False +always_document_param_types = False +typehints_document_rtype = False diff --git a/libraries.txt b/libraries.txt index 05a28d829..3bb5d6107 100644 --- a/libraries.txt +++ b/libraries.txt @@ -1,8 +1,8 @@ -Profiling Code: -https://docs.python.org/2/library/profile.html - -Compiling to machine code: -https://numba.pydata.org/ - -Multiprocessing: -https://github.com/soravux/scoop +Profiling Code: +https://docs.python.org/2/library/profile.html + +Compiling to machine code: +https://numba.pydata.org/ + +Multiprocessing: +https://github.com/soravux/scoop diff --git a/paper/README.md b/paper/README.md index 277eb40e5..b81295c18 100644 --- a/paper/README.md +++ b/paper/README.md @@ -1,5 +1,5 @@ -PyAutoLens JOSS Paper -===================== - -Paper accompanying [PyAutoGalaxy](https://github.com/PyAutoLabs/PyAutoGalaxy) for submission to the Journal of Open Source +PyAutoLens JOSS Paper +===================== + +Paper accompanying [PyAutoGalaxy](https://github.com/PyAutoLabs/PyAutoGalaxy) for submission to the Journal of Open Source Software (JOSS). \ No newline at end of file diff --git a/paper/paper.bib b/paper/paper.bib index eba78d1bf..d387d764d 100644 --- a/paper/paper.bib +++ b/paper/paper.bib @@ -1,673 +1,673 @@ -@article{astropy1, -Adsnote = {Provided by the SAO/NASA Astrophysics Data System}, -Adsurl = {http://adsabs.harvard.edu/abs/2013A%26A...558A..33A}, -Archiveprefix = {arXiv}, -Author = {{Astropy Collaboration} and {Robitaille}, T.~P. and {Tollerud}, E.~J. and {Greenfield}, P. and {Droettboom}, M. and {Bray}, E. and {Aldcroft}, T. and {Davis}, M. and {Ginsburg}, A. and {Price-Whelan}, A.~M. and {Kerzendorf}, W.~E. and {Conley}, A. and {Crighton}, N. and {Barbary}, K. and {Muna}, D. and {Ferguson}, H. and {Grollier}, F. and {Parikh}, M.~M. and {Nair}, P.~H. and {Unther}, H.~M. and {Deil}, C. and {Woillez}, J. and {Conseil}, S. and {Kramer}, R. and {Turner}, J.~E.~H. and {Singer}, L. and {Fox}, R. and {Weaver}, B.~A. and {Zabalza}, V. and {Edwards}, Z.~I. and {Azalee Bostroem}, K. and {Burke}, D.~J. and {Casey}, A.~R. and {Crawford}, S.~M. and {Dencheva}, N. and {Ely}, J. and {Jenness}, T. and {Labrie}, K. and {Lim}, P.~L. and {Pierfederici}, F. and {Pontzen}, A. and {Ptak}, A. and {Refsdal}, B. and {Servillat}, M. and {Streicher}, O.}, -Doi = {10.1051/0004-6361/201322068}, -Eid = {A33}, -Eprint = {1307.6212}, -Journal = {\aap}, -Keywords = {methods: data analysis, methods: miscellaneous, virtual observatory tools}, -Month = oct, -Pages = {A33}, -Primaryclass = {astro-ph.IM}, -Title = {{Astropy: A community Python package for astronomy}}, -Volume = 558, -Year = 2013, -Bdsk-Url-1 = {https://dx.doi.org/10.1051/0004-6361/201322068}} -@article{astropy2, -Adsnote = {Provided by the SAO/NASA Astrophysics Data System}, -Adsurl = {https://ui.adsabs.harvard.edu/#abs/2018AJ....156..123T}, -Author = {{Price-Whelan}, A.~M. and {Sip{\H{o}}cz}, B.~M. and {G{\"u}nther}, H.~M. and {Lim}, P.~L. and {Crawford}, S.~M. and {Conseil}, S. and {Shupe}, D.~L. and {Craig}, M.~W. and {Dencheva}, N. and {Ginsburg}, A. and {VanderPlas}, J.~T. and {Bradley}, L.~D. and {P{\'e}rez-Su{\'a}rez}, D. and {de Val-Borro}, M. and {Paper Contributors}, (Primary and {Aldcroft}, T.~L. and {Cruz}, K.~L. and {Robitaille}, T.~P. and {Tollerud}, E.~J. and {Coordination Committee}, (Astropy and {Ardelean}, C. and {Babej}, T. and {Bach}, Y.~P. and {Bachetti}, M. and {Bakanov}, A.~V. and {Bamford}, S.~P. and {Barentsen}, G. and {Barmby}, P. and {Baumbach}, A. and {Berry}, K.~L. and {Biscani}, F. and {Boquien}, M. and {Bostroem}, K.~A. and {Bouma}, L.~G. and {Brammer}, G.~B. and {Bray}, E.~M. and {Breytenbach}, H. and {Buddelmeijer}, H. and {Burke}, D.~J. and {Calderone}, G. and {Cano Rodr{\'\i}guez}, J.~L. and {Cara}, M. and {Cardoso}, J.~V.~M. and {Cheedella}, S. and {Copin}, Y. and {Corrales}, L. and {Crichton}, D. and {D{\textquoteright}Avella}, D. and {Deil}, C. and {Depagne}, {\'E}. and {Dietrich}, J.~P. and {Donath}, A. and {Droettboom}, M. and {Earl}, N. and {Erben}, T. and {Fabbro}, S. and {Ferreira}, L.~A. and {Finethy}, T. and {Fox}, R.~T. and {Garrison}, L.~H. and {Gibbons}, S.~L.~J. and {Goldstein}, D.~A. and {Gommers}, R. and {Greco}, J.~P. and {Greenfield}, P. and {Groener}, A.~M. and {Grollier}, F. and {Hagen}, A. and {Hirst}, P. and {Homeier}, D. and {Horton}, A.~J. and {Hosseinzadeh}, G. and {Hu}, L. and {Hunkeler}, J.~S. and {Ivezi{\'c}}, {\v{Z}}. and {Jain}, A. and {Jenness}, T. and {Kanarek}, G. and {Kendrew}, S. and {Kern}, N.~S. and {Kerzendorf}, W.~E. and {Khvalko}, A. and {King}, J. and {Kirkby}, D. and {Kulkarni}, A.~M. and {Kumar}, A. and {Lee}, A. and {Lenz}, D. and {Littlefair}, S.~P. and {Ma}, Z. and {Macleod}, D.~M. and {Mastropietro}, M. and {McCully}, C. and {Montagnac}, S. and {Morris}, B.~M. and {Mueller}, M. and {Mumford}, S.~J. and {Muna}, D. and {Murphy}, N.~A. and {Nelson}, S. and {Nguyen}, G.~H. and {Ninan}, J.~P. and {N{\"o}the}, M. and {Ogaz}, S. and {Oh}, S. and {Parejko}, J.~K. and {Parley}, N. and {Pascual}, S. and {Patil}, R. and {Patil}, A.~A. and {Plunkett}, A.~L. and {Prochaska}, J.~X. and {Rastogi}, T. and {Reddy Janga}, V. and {Sabater}, J. and {Sakurikar}, P. and {Seifert}, M. and {Sherbert}, L.~E. and {Sherwood-Taylor}, H. and {Shih}, A.~Y. and {Sick}, J. and {Silbiger}, M.~T. and {Singanamalla}, S. and {Singer}, L.~P. and {Sladen}, P.~H. and {Sooley}, K.~A. and {Sornarajah}, S. and {Streicher}, O. and {Teuben}, P. and {Thomas}, S.~W. and {Tremblay}, G.~R. and {Turner}, J.~E.~H. and {Terr{\'o}n}, V. and {van Kerkwijk}, M.~H. and {de la Vega}, A. and {Watkins}, L.~L. and {Weaver}, B.~A. and {Whitmore}, J.~B. and {Woillez}, J. and {Zabalza}, V. and {Contributors}, (Astropy}, -Doi = {10.3847/1538-3881/aabc4f}, -Eid = {123}, -Journal = {\aj}, -Keywords = {methods: data analysis, methods: miscellaneous, methods: statistical, reference systems, Astrophysics - Instrumentation and Methods for Astrophysics}, -Month = Sep, -Pages = {123}, -Primaryclass = {astro-ph.IM}, -Title = {{The Astropy Project: Building an Open-science Project and Status of the v2.0 Core Package}}, -Volume = {156}, -Year = 2018, -Bdsk-Url-1 = {https://doi.org/10.3847/1538-3881/aabc4f}} - -@article{colossus, -abstract = {This paper introduces Colossus, a public, open-source python package for calculations related to cosmology, the large-scale structure (LSS) of matter in the universe, and the properties of dark matter halos. The code is designed to be fast and easy to use, with a coherent, well-documented user interface. The cosmology module implements Friedman-Lemaitre-Robertson-Walker cosmologies including curvature, relativistic species, and different dark energy equations of state, and provides fast computations of the linear matter power spectrum, variance, and correlation function. The LSS module is concerned with the properties of peaks in Gaussian random fields and halos in a statistical sense, including their peak height, peak curvature, halo bias, and mass function. The halo module deals with spherical overdensity radii and masses, density profiles, concentration, and the splashback radius. To facilitate the rapid exploration of these quantities, Colossus implements more than 40 different fitting functions from the literature. I discuss the core routines in detail, with particular emphasis on their accuracy. Colossus is available at bitbucket.org/bdiemer/colossus.}, -archivePrefix = {arXiv}, -arxivId = {1712.04512}, -author = {Diemer, Benedikt}, -doi = {10.3847/1538-4365/aaee8c}, -eprint = {1712.04512}, -file = {:home/jammy/Documents/Papers/Software/Collosus2018.pdf:pdf}, -issn = {0067-0049}, -journal = {The Astrophysical Journal Supplement Series}, -keywords = {cosmology,cosmology: theory,methods: numerical,methods,numerical,theory}, -number = {2}, -pages = {35}, -publisher = {IOP Publishing}, -title = {{COLOSSUS: A Python Toolkit for Cosmology, Large-scale Structure, and Dark Matter Halos}}, -url = {http://dx.doi.org/10.3847/1538-4365/aaee8c}, -volume = {239}, -year = {2018} -} -@article{corner, - doi = {10.21105/joss.00024}, - url = {https://doi.org/10.21105/joss.00024}, - year = {2016}, - month = {jun}, - publisher = {The Open Journal}, - volume = {1}, - number = {2}, - pages = {24}, - author = {Daniel Foreman-Mackey}, - title = {corner.py: Scatterplot matrices in Python}, - journal = {The Journal of Open Source Software} -} -@article{dynesty, -abstract = {We present dynesty, a public, open-source, python package to estimate Bayesian posteriors and evidences (marginal likelihoods) using the dynamic nested sampling methods developed by Higson et al. By adaptively allocating samples based on posterior structure, dynamic nested sampling has the benefits of Markov chain Monte Carlo (MCMC) algorithms that focus exclusively on posterior estimation while retaining nested sampling's ability to estimate evidences and sample from complex, multimodal distributions. We provide an overview of nested sampling, its extension to dynamic nested sampling, the algorithmic challenges involved, and the various approaches taken to solve them in this and previous work. We then examine dynesty's performance on a variety of toy problems along with several astronomical applications. We find in particular problems dynesty can provide substantial improvements in sampling efficiency compared to popular MCMC approaches in the astronomical literature. More detailed statistical results related to nested sampling are also included in the appendix.}, -archivePrefix = {arXiv}, -arxivId = {1904.02180}, -author = {Speagle, Joshua S}, -doi = {10.1093/mnras/staa278}, -eprint = {1904.02180}, -file = {:home/jammy/Documents/Papers/PPLs/Dynesty.pdf:pdf}, -issn = {0035-8711}, -journal = {MNRAS}, -keywords = {data analysis,methods,statistical}, -number = {3}, -pages = {3132--3158}, -title = {{dynesty: a dynamic nested sampling package for estimating Bayesian posteriors and evidences}}, -volume = {493}, -year = {2020} -} -@article{emcee, -abstract = {We introduce a stable, well tested Python implementation of the affine-invariant ensemble sampler for Markov chain Monte Carlo (MCMC) proposed by Goodman {\&} Weare (2010). The code is open source and has already been used in several published projects in the astrophysics literature. The algorithm behind emcee has several advantages over traditional MCMC sampling methods and it has excellent performance as measured by the autocorrelation time (or function calls per independent sample). One major advantage of the algorithm is that it requires hand-tuning of only 1 or 2 parameters compared to {\$}\backslashsim N{\^{}}2{\$} for a traditional algorithm in an N-dimensional parameter space. In this document, we describe the algorithm and the details of our implementation and API. Exploiting the parallelism of the ensemble method, emcee permits any user to take advantage of multiple CPU cores without extra effort. The code is available online at http://dan.iel.fm/emcee under the MIT License.}, -archivePrefix = {arXiv}, -arxivId = {1202.3665}, -author = {Foreman-Mackey, Daniel and Hogg, David W. and Lang, Dustin and Goodman, Jonathan}, -doi = {10.1086/670067}, -eprint = {1202.3665}, -file = {:home/jammy/Documents/Papers/PPLs/Emcee.pdf:pdf}, -issn = {00046280}, -journal = {Publications of the Astronomical Society of the Pacific}, -number = {925}, -pages = {306--312}, -title = {{emcee : The MCMC Hammer}}, -volume = {125}, -year = {2013} -} -@article{matplotlib, - Author = {Hunter, J. D.}, - Title = {Matplotlib: A 2D graphics environment}, - Journal = {Computing in Science \& Engineering}, - Volume = {9}, - Number = {3}, - Pages = {90--95}, - abstract = {Matplotlib is a 2D graphics package used for Python for - application development, interactive scripting, and publication-quality - image generation across user interfaces and operating systems.}, - publisher = {IEEE COMPUTER SOC}, - doi = {10.1109/MCSE.2007.55}, - year = 2007 -} -@article{numba, -abstract = {Dynamic, interpreted languages, like Python, are attractive for domain-experts and scientists experimenting with new ideas. However, the performance of the interpreter is of-ten a barrier when scaling to larger data sets. This paper presents a just-in-time compiler for Python that focuses in scientific and array-oriented computing. Starting with the simple syntax of Python, Numba compiles a subset of the language into efficient machine code that is comparable in performance to a traditional compiled language. In addi-tion, we share our experience in building a JIT compiler using LLVM[1].}, -author = {Lam, Siu Kwan and Pitrou, Antoine and Seibert, Stanley}, -doi = {10.1145/2833157.2833162}, -file = {:home/jammy/Documents/Papers/Software/numba{\_}sc15.pdf:pdf}, -isbn = {9781450340052}, -journal = {Proceedings of the Second Workshop on the LLVM Compiler Infrastructure in HPC - LLVM '15}, -keywords = {2,a jit for numeric,com-,compiler,jit,just-in-time,llvm,numba is a function-at-a-time,python}, -pages = {1--6}, -title = {{Numba: a LLVM-based Python JIT compiler}}, -url = {http://dl.acm.org/citation.cfm?doid=2833157.2833162}, -year = {2015} -} -@article{numpy, - author={S. {van der Walt} and S. C. {Colbert} and G. {Varoquaux}}, - journal={Computing in Science Engineering}, - title={The NumPy Array2D: A Structure for Efficient Numerical Computation}, - year={2011}, - volume={13}, - number={2}, - pages={22-30}, - doi={10.1109/MCSE.2011.37}} - -@article{pyautofit, - doi = {10.21105/joss.02550}, - url = {https://doi.org/10.21105/joss.02550}, - year = {2021}, - publisher = {The Open Journal}, - volume = {6}, - number = {58}, - pages = {2550}, - author = {Nightingale, J. W. and Hayes, R. G. and Griffiths, M.}, - title = {`PyAutoFit`: A Classy Probabilistic Programming Language for Model Composition and Fitting}, - journal = {J. Open Source Softw.} -} -@article{multinest, -abstract = {We present further development and the first public release of our multimodal nested sampling algorithm, called MultiNest. This Bayesian inference tool calculates the evidence, with an associated error estimate, and produces posterior samples from distributions that may contain multiple modes and pronounced (curving) degeneracies in high dimensions. The developments presented here lead to further substantial improvements in sampling efficiency and robustness, as compared to the original algorithm presented in Feroz and Hobson, which itself significantly outperformed existing Markov chain Monte Carlo techniques in a wide range of astrophysical inference problems. The accuracy and economy of the MultiNest algorithm are demonstrated by application to two toy problems and to a cosmological inference problem focusing on the extension of the vanilla $\Lambda$ cold dark matter model to include spatial curvature and a varying equation of state for dark energy. The MultiNest software, which is fully parallelized using MPI and includes an interface to CosmoMC, is available at http://www.mrao.cam.ac.uk/software/multinest/. It will also be released as part of the SuperBayeS package, for the analysis of supersymmetric theories of particle physics, at http://www.superbayes.org. {\textcopyright} 2009 RAS.}, -archivePrefix = {arXiv}, -arxivId = {0809.3437}, -author = {Feroz, F. and Hobson, M. P. and Bridges, M.}, -doi = {10.1111/j.1365-2966.2009.14548.x}, -eprint = {0809.3437}, -isbn = {0035-8711}, -issn = {00358711}, -journal = {MNRAS}, -keywords = {Methods: Data analysis,Methods: Statistical}, -number = {4}, -pages = {1601--1614}, -pmid = {29176}, -title = {{MultiNest: An efficient and robust Bayesian inference tool for cosmology and particle physics}}, -volume = {398}, -year = {2009} -} -@article{pymultinest, -abstract = {Context. Aims. Active galactic nuclei are known to have complex X-ray spectra that depend on both the properties of the accrediting super-massive black hole (e.g. mass, accretion rate) and the distribution of obscuring material in its vicinity (i.e. the "torus"). Often however, simple and even unphysical models are adopted to represent the X-ray spectra of AGN, which do not capture the complexity and diversity of the observations. In the case of blank field surveys in particular, this should have an impact on e.g. the determination of the AGN luminosity function, the inferred accretion history of the Universe and also on our understanding of the relation between AGN and their host galaxies. Methods. We develop a Bayesian framework for model comparison and parameter estimation of X-ray spectra. We take into account uncertainties associated with both the Poisson nature of X-ray data and the determination of source redshift using photometric methods. We also demonstrate how Bayesian model comparison can be used to select among ten different physically motivated X-ray spectral models the one that provides a better representation of the observations. This methodology is applied to X-ray AGN in the 4 Ms Chandra Deep Field South. Results. For the {\~{}}350 AGN in that field, our analysis identifies four components needed to represent the diversity of the observed X-ray spectra: (1) an intrinsic power law; (2) a cold obscurer which reprocesses the radiation due to photo-electric absorption, Compton scattering and Fe-K fluorescence; (3) an unabsorbed power law associated with Thomson scattering off ionised clouds; and (4) Compton reflection, most noticeable from a stronger-than-expected Fe-K line. Simpler models, such as a photo-electrically absorbed power law with a Thomson scattering component, are ruled out with decisive evidence (B {\textgreater} 100). We also find that ignoring the Thomson scattering component results in underestimation of the inferred column density, NH, of the obscurer. Regarding the geometry of the obscurer, there is strong evidence against both a completely closed (e.g. sphere), or entirely open (e.g. blob of material along the line of sight), toroidal geometry in favour of an intermediate case. Conclusions. Despite the use of low-count spectra, our methodology is able to draw strong inferences on the geometry of the torus. Simpler models are ruled out in favour of a geometrically extended structure with significant Compton scattering. We confirm the presence of a soft component, possibly associated with Thomson scattering off ionised clouds in the opening angle of the torus. The additional Compton reflection required by data over that predicted by toroidal geometry models, may be a sign of a density gradient in the torus or reflection off the accretion disk. Finally, we release a catalogue of AGN in the CDFS with estimated parameters such as the accretion luminosity in the 2-10 keV band and the column density, NH, of the obscurer. {\textcopyright} ESO, 2014.}, -archivePrefix = {arXiv}, -arxivId = {1402.0004}, -author = {Buchner, J. and Georgakakis, A. and Nandra, K. and Hsu, L. and Rangel, C. and Brightman, M. and Merloni, A. and Salvato, M. and Donley, J. and Kocevski, D.}, -doi = {10.1051/0004-6361/201322971}, -eprint = {1402.0004}, -file = {:home/jammy/Documents/Papers/Stats/ButchnerPyMultiNest.pdf:pdf}, -issn = {14320746}, -journal = {A&A}, -keywords = {Accretion, accretion disks,galaxies: high-redshift,galaxies: nuclei,Methods: data analysis,Methods: statistical,X-rays: galaxies}, -pages = {A125}, -title = {{X-ray spectral modelling of the AGN obscuring region in the CDFS: Bayesian model selection and catalogue}}, -volume = {564}, -year = {2014} -} -@article{pynufft, -abstract = {A Python non-uniform fast Fourier transform (PyNUFFT) package has been developed to accelerate multidimensional non-Cartesian image reconstruction on heterogeneous platforms. Since scientific computing with Python encompasses a mature and integrated environment, the time efficiency of the NUFFT algorithm has been a major obstacle to real-time non-Cartesian image reconstruction with Python. The current PyNUFFT software enables multi-dimensional NUFFT accelerated on a heterogeneous platform, which yields an efficient solution to many non-Cartesian imaging problems. The PyNUFFT also provides several solvers, including the conjugate gradient method, 1 total variation regularized ordinary least square (L1TV-OLS), and 1 total variation regularized least absolute deviation (L1TV-LAD). Metaprogramming libraries have been employed to accelerate PyNUFFT. The PyNUFFT package has been tested on multi-core central processing units (CPUs) and graphic processing units (GPUs), with acceleration factors of 6.3–9.5× on a 32-thread CPU platform and 5.4–13× on a GPU.}, -author = {Lin, Jyh Miin}, -doi = {10.3390/jimaging4030051}, -file = {:home/jammy/Documents/Papers/Software/jimaging-04-00051-v2.pdf:pdf}, -issn = {2313433X}, -journal = {Journal of Imaging}, -keywords = {Graphic processing unit (GPU),Heterogeneous system architecture (HSA),Magnetic resonance imaging (MRI),Multi-core system,Total variation (TV)}, -number = {3}, -pages = {1--22}, -title = {{Python non-uniform fast fourier transform (PyNUFFT): An accelerated non-cartesian MRI package on a heterogeneous platform (CPU/GPU)}}, -volume = {4}, -year = {2018} -} -@software{pyquad, - author = {Ashley J. Kelly}, - title = {pyquad}, - month = jul, - year = 2020, - publisher = {Zenodo}, - version = {0.6.4}, - doi = {10.5281/zenodo.3936959}, - url = {https://doi.org/10.5281/zenodo.3936959} -} -@article{pyswarms, - author = {Lester James V. Miranda}, - title = "{P}y{S}warms, a research-toolkit for {P}article {S}warm {O}ptimization in {P}ython", - journal = {J. Open Source Softw.}, - year = {2018}, - volume = {3}, - issue = {21}, - doi = {10.21105/joss.00433}, - url = {https://doi.org/10.21105/joss.00433} -} - @book{python, - author = {Van Rossum, Guido and Drake, Fred L.}, - title = {Python 3 Reference Manual}, - year = {2009}, - isbn = {1441412697}, - publisher = {CreateSpace}, - address = {Scotts Valley, CA} -} -@article{scikit-image, - title={scikit-image: image processing in Python}, - author={Van der Walt, Stefan and Sch{\"o}nberger, Johannes L and Nunez-Iglesias, Juan and Boulogne, Fran{\c{c}}ois and Warner, Joshua D and Yager, Neil and Gouillart, Emmanuelle and Yu, Tony}, - journal={PeerJ}, - volume={2}, - pages={e453}, - year={2014}, - publisher={PeerJ Inc.} -} -@article{scikit-learn, - title={Scikit-learn: Machine Learning in {P}ython}, - author={Pedregosa, F. and Varoquaux, G. and Gramfort, A. and Michel, V. - and Thirion, B. and Grisel, O. and Blondel, M. and Prettenhofer, P. - and Weiss, R. and Dubourg, V. and Vanderplas, J. and Passos, A. and - Cournapeau, D. and Brucher, M. and Perrot, M. and Duchesnay, E.}, - journal={Journal of Machine Learning Research}, - volume={12}, - pages={2825--2830}, - year={2011} -} -@article{scipy, - author = {{Virtanen}, Pauli and {Gommers}, Ralf and {Oliphant}, - Travis E. and {Haberland}, Matt and {Reddy}, Tyler and - {Cournapeau}, David and {Burovski}, Evgeni and {Peterson}, Pearu - and {Weckesser}, Warren and {Bright}, Jonathan and {van der Walt}, - St{\'e}fan J. and {Brett}, Matthew and {Wilson}, Joshua and - {Jarrod Millman}, K. and {Mayorov}, Nikolay and {Nelson}, Andrew - R.~J. and {Jones}, Eric and {Kern}, Robert and {Larson}, Eric and - {Carey}, CJ and {Polat}, {\.I}lhan and {Feng}, Yu and {Moore}, - Eric W. and {Vand erPlas}, Jake and {Laxalde}, Denis and - {Perktold}, Josef and {Cimrman}, Robert and {Henriksen}, Ian and - {Quintero}, E.~A. and {Harris}, Charles R and {Archibald}, Anne M. - and {Ribeiro}, Ant{\^o}nio H. and {Pedregosa}, Fabian and - {van Mulbregt}, Paul and {Contributors}, SciPy 1. 0}, - title = "{SciPy 1.0: Fundamental Algorithms for Scientific - Computing in Python}", - journal = {Nature Methods}, - year = "2020", - volume={17}, - pages={261--272}, - adsurl = {https://rdcu.be/b08Wh}, - doi = {10.1038/s41592-019-0686-2}, -} -@article{Alexander2019, -abstract = {Dark matter substructure has the potential to discriminate between broad classes of dark matter models. With this in mind, we construct novel solutions to the equations of motion governing condensate dark matter candidates, namely axion Bose-Einstein condensates and superfluids. These solutions are highly compressed along one axis and thus have a disk-like geometry. We discuss linear stability of these solutions, consider the astrophysical implications as a large-scale dark disk or as small scale substructure, and find a characteristic signal in strong gravitational lensing. This adds to the growing body of work that indicates that the morphology of dark matter substructure is a powerful probe of the nature of dark matter.}, -archivePrefix = {arXiv}, -arxivId = {1901.03694}, -author = {Alexander, Stephon and Bramburger, Jason J. and McDonough, Evan}, -doi = {10.1016/j.physletb.2019.134871}, -eprint = {1901.03694}, -file = {:home/jammy/Documents/Papers/PyAutoLens/Alexander2018DiskSuperfliod.pdf:pdf}, -issn = {03702693}, -journal = {Physics Letters, Section B: Nuclear, Elementary Particle and High-Energy Physics}, -pages = {1--7}, -title = {{Dark disk substructure and superfluid dark matter}}, -url = {http://arxiv.org/abs/1901.03694}, -volume = {797}, -year = {2019} -} -@article{Bolton2012, -abstract = {We present an analysis of the evolution of the central mass-density profile of massive elliptical galaxies from the SLACS and BELLS strong gravitational lens samples over the redshift interval z ≈ 0.1-0.6, based on the combination of strong-lensing aperture mass and stellar velocity-dispersion constraints. We find a significant trend toward steeper mass profiles (parameterized by the power-law density model with $\rho$ ∝ r-$\gamma$) at later cosmic times, with magnitude d 〈$\gamma$〉/dz = -0.60 ± 0.15. We show that the combined lens-galaxy sample is consistent with a non-evolving distribution of stellar velocity dispersions. Considering possible additional dependence of 〈$\gamma$〉 on lens-galaxy stellar mass, effective radius, and S{\'{e}}rsic index, we find marginal evidence for shallower mass profiles at higher masses and larger sizes, but with a significance that is subdominant to the redshift dependence. Using the results of published Monte Carlo simulations of spectroscopic lens surveys, we verify that our mass-profile evolution result cannot be explained by lensing selection biases as a function of redshift. Interpreted as a true evolutionary signal, our result suggests that major dry mergers involving off-axis trajectories play a significant role in the evolution of the average mass-density structure of massive early-type galaxies over the past 6Gyr. We also consider an alternative non-evolutionary hypothesis based on variations in the strong-lensing measurement aperture with redshift, which would imply the detection of an "inflection zone" marking the transition between the baryon-dominated and dark-matter halo-dominated regions of the lens galaxies. Further observations of the combined SLACS+BELLS sample can constrain this picture more precisely, and enable a more detailed investigation of the multivariate dependences of galaxy mass structure across cosmic time. {\textcopyright} 2012. The American Astronomical Society. All rights reserved.}, -archivePrefix = {arXiv}, -arxivId = {1201.2988}, -author = {Bolton, Adam S. and Brownstein, Joel R. and Kochanek, Christopher S. and Shu, Yiping and Schlegel, David J. and Eisenstein, Daniel J. and Wake, David A. and Connolly, Natalia and Maraston, Claudia and Arneson, Ryan A. and Weaver, Benjamin A.}, -doi = {10.1088/0004-637X/757/1/82}, -eprint = {1201.2988}, -issn = {15384357}, -journal = {Astrophysical Journal}, -keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,galaxies: structure,Gravitational lensing: strong}, -number = {1}, -pages = {82}, -title = {{The BOSS emission-line lens survey. II. Investigating mass-density profile evolution in the SLACS+BELLS strong gravitational lens sample}}, -url = {http://arxiv.org/abs/1201.2988}, -volume = {757}, -year = {2012} -} -@article{Collett2015, -abstract = {Ongoing and future imaging surveys represent significant improvements in depth, area, and seeing compared to current data sets. These improvements offer the opportunity to discover up to three orders of magnitude more galaxy-galaxy strong lenses than are currently known. In this work we forecast the number of lenses that will be discoverable in forthcoming surveys and simulate their properties. We generate a population of statistically realistic strong lenses and simulate observations of this population for the Dark Energy Survey (DES), the Large Synoptic Survey Telescope (LSST), and Euclid surveys. We verify our model against the galaxy-scale lens search of the Canada-France-Hawaii Telescope Legacy Survey, predicting 250 discoverable lenses compared to 220 found by Gavazzi et al. The predicted Einstein radius distribution is also remarkably similar to that found by Sonnenfeld et al. For future surveys we find that, assuming Poisson limited lens galaxy subtraction, searches of the DES, LSST, and Euclid data sets should discover 2400, 120000, and 170000 galaxy-galaxy strong lenses, respectively. Finders using blue-minus-red (g - i) difference imaging for lens subtraction can discover 1300 and 62000 lenses in DES and LSST. The uncertainties on the model are dominated by the high-redshift source population, which typically gives fractional errors on the discoverable lens number at the level of tens of percent. We find that doubling the signal-to-noise ratio required for a lens to be detectable approximately halves the number of detectable lenses in each survey, indicating the importance of understanding the selection function and the sensitivity of future lens finders in interpreting strong lens statistics. We make our population forecasting and simulated observation codes publicly available so that the selection function of strong lens finders can easily be calibrated.}, -archivePrefix = {arXiv}, -arxivId = {1507.02657}, -author = {Collett, Thomas E.}, -doi = {10.1088/0004-637X/811/1/20}, -eprint = {1507.02657}, -isbn = {0004-637x}, -issn = {15384357}, -journal = {Astrophysical Journal}, -keywords = {gravitational lensing: strong}, -number = {1}, -pages = {20}, -title = {{The population of galaxy-galaxy strong lenses in forthcoming optical imaging surveys}}, -url = {http://arxiv.org/abs/1507.02657}, -volume = {811}, -year = {2015} -} -@article{Czoske2012, -abstract = {This paper presents the full Very Large Telescope (VLT)/VIMOS-IFU data set and related data products from an ESO Large Programme with the observational goal of obtaining two-dimensional kinematic data of early-type lens galaxies, out to one effective radius. The sample consists of 17 early-type galaxies (ETGs) selected from the SLACS gravitational-lens survey. The galaxies cover the redshift range from 0.08 to 0.35 and have stellar velocity dispersions between 200 and 350 kms-1. This programme is complemented by a similar observational programme on Keck, using long-slit spectroscopy. In combination with multi-band imaging data, the kinematic data provide stringent constraints on the inner mass profiles of ETGs beyond the local Universe. Our Large Programme thus extends studies of nearby ETGs (e.g. SAURON/ATLAS3D) by an order of magnitude in distance and towards higher masses. We provide an overview of our observational strategy, the data products (luminosity-weighted spectra andHubble Space Telescopeimages) and derived products (i.e. two-dimensional fields of velocity dispersions and streaming motions) that have been used in a number of published and forthcoming lensing, kinematic and stellar-population studies. These studies also pave the way for future studies of ETGs atz≈ 1 with the upcoming extremely large telescopes. {\textcopyright} 2011 The Authors MNRAS {\textcopyright} 2011 RAS.}, -archivePrefix = {arXiv}, -arxivId = {1108.0577}, -author = {Czoske, Oliver and Barnab{\`{e}}, Matteo and Koopmans, L{\'{e}}on V.E. and Treu, Tommaso and Bolton, Adam S.}, -doi = {10.1111/j.1365-2966.2011.19726.x}, -eprint = {1108.0577}, -isbn = {00358711}, -issn = {00358711}, -journal = {MNRAS}, -keywords = {galaxies: elliptical and lenticular, cD,galaxies: kinematics and dynamics,galaxies: structure,Gravitational lensing: strong,Techniques: spectroscopic}, -number = {1}, -pages = {656--668}, -title = {{Two-dimensional kinematics of SLACS lenses - IV. The complete VLT-VIMOS data set}}, -volume = {419}, -year = {2012} -} -@article{Dye2014, -abstract = {We have determined the mass density radial profiles of the first five strong gravitational lens systems discovered by the Herschel Astrophysical Terahertz Large Area Survey. We present an enhancement of the semilinear lens inversion method of Warren {\&} Dye which allows simultaneous reconstruction of several different wavebands and apply this to dual-band imaging of the lenses acquired with the Hubble Space Telescope. The five systems analysed here have lens redshifts which span a range 0.22 ≤ z ≤ 0.94. Our findings are consistent with other studies by concluding that: (1) the logarithmic slope of the total mass density profile steepens with decreasing redshift; (2) the slope is positively correlated with the average total projected mass density of the lens contained within half the effective radius and negatively correlated with the effective radius; (3) the fraction of dark matter contained within half the effective radius increases with increasing effective radius and increases with redshift. {\textcopyright} 2014 The Authors Published by Oxford University Press on behalf of the Royal Astronomical Society.}, -archivePrefix = {arXiv}, -arxivId = {1311.5893}, -author = {Dye, S. and Negrello, M. and Hopwood, R. and Nightingale, J. W. and Bussmann, R. S. and Amber, S. and Bourne, N. and Cooray, A. and Dariush, A. and Dunne, L. and Eales, S. A. and Gonzalez-Nuevo, J. and Ibar, E. and Ivison, R. J. and Maddox, S. and Valiante, E. and Smith, M.}, -doi = {10.1093/mnras/stu305}, -eprint = {1311.5893}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {galaxies: evolution,galaxies: structure}, -number = {3}, -pages = {2013--2025}, -title = {{Herschel*-ATLAS: Modelling the first strong gravitational lenses}}, -volume = {440}, -year = {2014} -} -@article{Enia2018, -abstract = {We perform lens modelling and source reconstruction of Sub-millimetre Array2D (SMA) data for a sample of 12 strongly lensed galaxies selected at 500$\mu$m in the Herschel Astrophysical Terahertz Large Area Survey (H-ATLAS). A previous analysis of the same data set used a single S{\'{e}}rsic profile to model the light distribution of each background galaxy. Here we model the source brightness distribution with an adaptive pixel scale scheme, extended to work in the Fourier visibility space of interferometry. We also present new SMA observations for seven other candidate lensed galaxies from theH-ATLAS sample. Our derived lens model parameters are in general consistent with previous findings. However, our estimated magnification factors, ranging from 3 to 10, are lower. The discrepancies are observed in particular where the reconstructed source hints at the presence of multiple knots of emission.We define an effective radius of the reconstructed sources based on the area in the source plane where emission is detected above 5s. We also fit the reconstructed source surface brightness with an elliptical Gaussian model. We derive a median value reff {\~{}} 1.77 kpc and a median Gaussian full width at half-maximum {\~{}}1.47 kpc. After correction for magnification, our sources have intrinsic star formation rates (SFR) {\~{}} 900-3500M⊙ yr-1, resulting in a median SFR surface density $\Sigma$SFR {\~{}} 132M⊙ yr-1 kpc-2 (or {\~{}}218M⊙ yr-1 kpc-2 for the Gaussian fit). This is consistent with that observed for other star-forming galaxies at similar redshifts, and is significantly below the Eddington limit for a radiation pressure regulated starburst.}, -archivePrefix = {arXiv}, -arxivId = {1801.01831}, -author = {Enia, A. and Negrello, M. and Gurwell, M. and Dye, S. and Rodighiero, G. and Massardi, M. and {De Zotti}, G. and Franceschini, A. and Cooray, A. and van der Werf, P. and Birkinshaw, M. and Michalowski, M. J. and Oteo, I.}, -doi = {10.1093/mnras/sty021}, -eprint = {1801.01831}, -file = {:home/jammy/.local/share/data/Mendeley Ltd./Mendeley Desktop/Downloaded/Enia et al. - 2018 - The Herschel-ATLAS Magnifications and physical sizes of 500-$\mu$m-selected strongly lensed galaxies.pdf:pdf}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {galaxies: structure,Gravitational lensing: strong,Instrumentation: interferometers}, -number = {3}, -pages = {3467--3484}, -title = {{The Herschel-ATLAS: Magnifications and physical sizes of 500-$\mu$m-selected strongly lensed galaxies}}, -url = {http://arxiv.org/abs/1801.01831}, -volume = {475}, -year = {2018} -} -@article{Hermans2019, -abstract = {Posterior inference with an intractable likelihood is becoming an increasingly common task in scientific domains which rely on sophisticated computer simulations. Typically, these mechanistic models do not admit tractable densities forcing practitioners to rely on approximations during inference. This work proposes a novel approach to address the intractability of the likelihood and the marginal model. We achieve this by learning a flexible estimator which approximates the likelihood-to-evidence ratio. The resulting amortized ratio estimator is embedded in MCMC samplers such as Metropolis-Hastings and Hamiltonian Monte Carlo to approximate the likelihood-ratio between consecutive states in the Markov chain, allowing us to draw samples from the intractable posterior. Techniques are presented to improve the numerical stability. We demonstrate our approach on a variety of benchmarks and compare against well-established approximate inference techniques. Scientific applications in high energy and astrophysics with high-dimensional observations show its applicability.}, -archivePrefix = {arXiv}, -arxivId = {1903.04057}, -author = {Hermans, Joeri and Begy, Volodimir and Louppe, Gilles}, -eprint = {1903.04057}, -file = {:home/jammy/Documents/Papers/PyAutoLens/Hermanns2019LikelihoodFree.pdf:pdf}, -number = {i}, -title = {{Likelihood-free MCMC with Amortized Approximate Likelihood Ratios}}, -url = {http://arxiv.org/abs/1903.04057}, -year = {2019} -} -@article{Koopmans2009, -abstract = {Based on 58 SLACS strong-lens early-type galaxies (ETGs) with direct total-mass and stellar-velocity dispersion measurements, we find that inside one effective radius massive elliptical galaxies with M eff ≳ 3 × 1010 M⊙ are well approximated by a power-law ellipsoid, with an average logarithmic density slope of 〈$\gamma$′ LD〉 ≡ -dlog($\rho$tot)/dlog(r) = 2.085 +0.025-0.018 (random error on mean) for isotropic orbits with $\beta$r = 0, 0.1 (syst.) and intrinsic scatter (all errors indicate the 68{\%} CL). We find no correlation of $\gamma$′LD with galaxy mass (M eff), rescaled radius (i.e., R einst/R eff) or redshift, despite intrinsic differences in density-slope between galaxies. Based on scaling relations, the average logarithmic density slope can be derived in an alternative manner, fully independent from dynamics, yielding 〈$\gamma$′SR〉 = 1.959 0.077. Agreement between the two values is reached for 〈$\beta$r〉 = 0.45 0.25, consistent with mild radial anisotropy. This agreement supports the robustness of our results, despite the increase in mass-to-light ratio with total galaxy mass: M eff L 1.3630.056V,eff. We conclude that massive ETGs are structurally close to homologous with close to isothermal total density profiles (≲10{\%} intrinsic scatter) and have at most some mild radial anisotropy. Our results provide new observational limits on galaxy formation and evolution scenarios, covering 4 Gyr look-back time. {\textcopyright} 2009. The American Astronomical Society.}, -archivePrefix = {arXiv}, -arxivId = {0906.1349}, -author = {Koopmans, L. V.E. and Bolton, A. and Treu, T. and Czoske, O. and Auger, M. W. and Barnab{\`{e}}, M. and Vegetti, S. and Gavazzi, R. and Moustakas, L. A. and Burles, S.}, -doi = {10.1088/0004-637X/703/1/L51}, -eprint = {0906.1349}, -isbn = {1522-1601 (Electronic)$\backslash$r0161-7567 (Linking)}, -issn = {15384357}, -journal = {Astrophysical Journal}, -keywords = {galaxies: structure,Gravitational lensing}, -number = {1 PART 2}, -pages = {L51--L54}, -pmid = {25414243}, -title = {{The structure and dynamics of massive early-type galaxies: On homology, isothermality, and isotropy inside one effective radius}}, -url = {http://stacks.iop.org/1538-4357/703/i=1/a=L51?key=crossref.0217bdab14c5f868ab70f18136b0acdd}, -volume = {703}, -year = {2009} -} -@article{McCully2014, -abstract = {In strong gravitational lens systems, the light bending is usually dominated by one main galaxy, but may be affected by other mass along the line of sight (LOS). Shear and convergence can be used to approximate the contributions from less significant perturbers (e.g. those that are projected far from the lens or have a small mass), but higher order effects need to be included for objects that are closer or more massive. We develop a framework for multiplane lensing that can handle an arbitrary combination of tidal planes treated with shear and convergence and planes treated exactly (i.e. including higher order terms). This framework addresses all of the traditional lensing observables including image positions, fluxes, and time delays to facilitate lens modelling that includes the non-linear effects due to mass along the LOS. It balances accuracy (accounting for higher order terms when necessary) with efficiency (compressing all other LOS effects into a set of matrices that can be calculated up front and cached for lens modelling). We identify a generalized multiplane mass sheet degeneracy, in which the effective shear and convergence are sums over the lensing planes with specific, redshift-dependent weighting factors. {\textcopyright} 2014 The Authors Published by Oxford University Press on behalf of the Royal Astronomical Society.}, -archivePrefix = {arXiv}, -arxivId = {1401.0197}, -author = {McCully, Curtis and Keeton, Charles R. and Wong, Kenneth C. and Zabludoff, Ann I.}, -doi = {10.1093/mnras/stu1316}, -eprint = {1401.0197}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {Gravitational lensing: strong,Gravitational lensing: weak}, -month = {oct}, -number = {4}, -pages = {3631--3642}, -title = {{A new hybrid framework to efficiently model lines of sight to gravitational lenses}}, -volume = {443}, -year = {2014} -} -@article{Negrello2014, -abstract = {We report on deep near-infrared observations obtained with the Wide Field Camera-3 (WFC3) onboard the Hubble Space Telescope (HST) of the first five confirmed gravitational lensing events discovered by the Herschel Astrophysical Terahertz Large Area Survey (H-ATLAS). We succeed in disentangling the background galaxy from the lens to gain separate photometry of the two components. The HST data allow us to significantly improve on previous constraints of the mass in stars of the lensed galaxy and to perform accurate lens modelling of these systems, as described in the accompanying paper by Dye et al. We fit the spectral energy distributions of the background sources from near-IR to millimetre wavelengths and use the magnification factors estimated by Dye et al. to derive the intrinsic properties of the lensed galaxies. We find these galaxies to have star-formations rates (SFR) {\~{}} 400-2000 M⊙ yr-1, with {\~{}}(6-25) × 1010 M⊙ of their baryonic mass already turned into stars. At these rates of star formation, all remaining molecular gas will be exhausted in less than {\~{}}100 Myr, reaching a final mass in stars of a few 1011 M⊙. These galaxies are thus proto-ellipticals caught during their major episode of star formation, and observed at the peak epoch (z {\~{}} 1.5-3) of the cosmic star formation history of the Universe.}, -archivePrefix = {arXiv}, -arxivId = {1311.5898}, -author = {Negrello, M. and Hopwood, R. and Dye, S. and da Cunha, E. and Serjeant, S. and Fritz, J. and Rowlands, K. and Fleuren, S. and Bussmann, R. S. and Cooray, A. and Dannerbauer, H. and Gonzalez-Nuevo, J. and Lapi, A. and Omont, A. and Amber, S. and Auld, R. and Baes, M. and Buttiglione, S. and Cava, A. and Danese, L. and Dariush, A. and {De Zotti}, G. and Dunne, L. and Eales, S. and Ibar, E. and Ivison, R. J. and Kim, S. and Leeuw, L. and Maddox, S. and Michalowski, M. J. and Massardi, M. and Pascale, E. and Pohlen, M. and Rigby, E. and Smith, D. J.B. and Sutherland, W. and Temi, P. and Wardlow, J.}, -doi = {10.1093/mnras/stu413}, -eprint = {1311.5898}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,galaxies: formation,Gravitational lensing: strong,Infrared: galaxies,Submillimetre: galaxies}, -number = {3}, -pages = {1999--2012}, -title = {{Herschel *-ATLAS: Deep HST/WFC3 imaging of strongly lensed submillimetre galaxies}}, -url = {http://arxiv.org/abs/1311.5898}, -volume = {440}, -year = {2014} -} -@article{Nightingale2015, -abstract = {We present a new pixelized method for the inversion of gravitationally lensed extended source images which we term adaptive semi-linear inversion (SLI). At the heart of the method is an h-means clustering algorithm which is used to derive a source plane pixelization that adapts to the lens model magnification. The distinguishing feature of adaptive SLI is that every pixelization is derived from a random initialization, ensuring that data discretization is performed in a completely different and unique way for every model parameter set. We compare standard SLI on a fixed source pixel grid with the new method and demonstrate the shortcomings of the former when modelling singular power-law ellipsoid (SPLE) lens profiles. In particular, we demonstrate the superior reliability and efficiency of adaptive SLI which, by design, fixes the number of degrees of freedom (NDOF) of the optimization and thereby removes biases present with other methods that allow the NDOF to vary. In addition, we highlight the importance of data discretization in pixel-based inversion methods, showing that adaptive SLI averages over significant systematics that are present when a fixed source pixel grid is used. In the case of the SPLE lens profile, we show how the method successfully samples its highly degenerate posterior probability distribution function with a single nonlinear search. The robustness of adaptive SLI provides a firm foundation for the development of a strong lens modelling pipeline, which will become necessary in the short-term future to cope with the increasing rate of discovery of new strong lens systems.}, -archivePrefix = {arXiv}, -arxivId = {1412.7436}, -author = {Nightingale, J. W. and Dye, S.}, -doi = {10.1093/mnras/stv1455}, -eprint = {1412.7436}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {galaxies: evolution,galaxies: structure,Methods: observational}, -month = {sep}, -number = {3}, -pages = {2940--2959}, -title = {{Adaptive semi-linear inversion of strong gravitational lens imaging}}, -volume = {452}, -year = {2015} -} -@article{Nightingale2018, -abstract = {This work presents AutoLens, the first entirely automated modeling suite for the analysis of galaxy-scale strong gravitational lenses. AutoLens simultaneously models the lens galaxy's light and mass whilst reconstructing the extended source galaxy on an adaptive pixel-grid. The method's approach to source-plane discretization is amorphous, adapting its clustering and regularization to the intrinsic properties of the lensed source. The lens's light is fitted using a superposition of Sersic functions, allowing AutoLens to cleanly deblend its light from the source. Single-component mass models representing the lens's total mass density profile are demonstrated, which in conjunction with light modeling can detect central images using a centrally cored profile. Decomposed mass modeling is also shown, which can fully decouple a lens's light and dark matter and determine whether the two components are geometrically aligned. The complexity of the light and mass models is automatically chosen via Bayesian model comparison. These steps form AutoLens's automated analysis pipeline, such that all results in this work are generated without any user intervention. This is rigorously tested on a large suite of simulated images, assessing its performance on a broad range of lens profiles, source morphologies, and lensing geometries. The method's performance is excellent, with accurate light, mass, and source profiles inferred for data sets representative of both existing Hubble imaging and future Euclid wide-field observations.}, -archivePrefix = {arXiv}, -arxivId = {1708.07377}, -author = {Nightingale, J. W. and Dye, S. and Massey, Richard J.}, -doi = {10.1093/mnras/sty1264}, -eprint = {1708.07377}, -file = {:home/jammy/Documents/Papers{\_}Me/AutoLensChangesMarked.pdf:pdf}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {Galaxy: structure,Gravitational lensing,Methods: data analysis}, -number = {4}, -pages = {4738--4784}, -title = {{AutoLens: Automated modeling of a strong lens's light, mass, and source}}, -url = {https://academic.oup.com/mnras/article/478/4/4738/5001434}, -volume = {478}, -year = {2018} -} -@article{Nightingale2019, -abstract = {We investigate how strong gravitational lensing can test contemporary models of massive elliptical (ME) galaxy formation, by combining a traditional decomposition of their visible stellar distribution with a lensing analysis of their mass distribution. As a proof of concept, we study a sample of three ME lenses, observing that all are composed of two distinct baryonic structures, a 'red' central bulge surrounded by an extended envelope of stellar material. Whilst these two components look photometrically similar, their distinct lensing effects permit a clean decomposition of their mass structure. This allows us to infer two key pieces of information about each lens galaxy: (i) the stellar mass distribution (without invoking stellar populations models) and (ii) the inner dark matter halo mass. We argue that these two measurements are crucial to testing models of ME formation, as the stellar mass profile provides a diagnostic of baryonic accretion and feedback whilst the dark matter mass places each galaxy in the context of LCDM large-scale structure formation. We also detect large rotational offsets between the two stellar components and a lopsidedness in their outer mass distributions, which hold further information on the evolution of each ME. Finally, we discuss how this approach can be extended to galaxies of all Hubble types and what implication our results have for studies of strong gravitational lensing.}, -archivePrefix = {arXiv}, -arxivId = {1901.07801}, -author = {Nightingale, J. W. and Massey, Richard J. and Harvey, David R. and Cooper, Andrew P. and Etherington, Amy and Tam, Sut Ieng and Hayes, Richard G.}, -doi = {10.1093/mnras/stz2220}, -eprint = {1901.07801}, -file = {:home/jammy/Documents/Papers{\_}Me/Gal{\_}Structure{\_}Final/GalaxyStructure.pdf:pdf}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {galaxies: Evolution,galaxies: Formation,Gravitational lensing: Strong}, -number = {2}, -pages = {2049--2068}, -title = {{Galaxy structure with strong gravitational lensing: Decomposing the internal mass distribution of massive elliptical galaxies}}, -url = {http://arxiv.org/abs/1901.07801}, -volume = {489}, -year = {2019} -} -@article{Sonnenfeld2015, -abstract = {We investigate the cosmic evolution of the internal structure of massive early-type galaxies over half of the age of the universe. We perform a joint lensing and stellar dynamics analysis of a sample of 81 strong lenses from the Strong Lensing Legacy Survey and Sloan ACS Lens Survey and combine the results with a hierarchical Bayesian inference method to measure the distribution of dark matter mass and stellar initial mass function (IMF) across the population of massive early-type galaxies. Lensing selection effects are taken into account. We find that the dark matter mass projected within the inner 5 kpc increases for increasing redshift, decreases for increasing stellar mass density, but is roughly constant along the evolutionary tracks of early-type galaxies. The average dark matter slope is consistent with that of a Navarro-Frenk-White profile, but is not well constrained. The stellar IMF normalization is close to a Salpeter IMF at log M ∗ = 11.5 and scales strongly with increasing stellar mass. No dependence of the IMF on redshift or stellar mass density is detected. The anti-correlation between dark matter mass and stellar mass density supports the idea of mergers being more frequent in more massive dark matter halos.}, -archivePrefix = {arXiv}, -arxivId = {1410.1881}, -author = {Sonnenfeld, Alessandro and Treu, Tommaso and Marshall, Philip J. and Suyu, Sherry H. and Gavazzi, Rapha{\"{e}}l and Auger, Matthew W. and Nipoti, Carlo}, -doi = {10.1088/0004-637X/800/2/94}, -eprint = {1410.1881}, -issn = {15384357}, -journal = {Astrophysical Journal}, -keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,gravitational lensing: strong}, -number = {2}, -pages = {94}, -title = {{The sl2s galaxy-scale lens sample. V. Dark matter halos and stellar imf of massive early-type galaxies out to redshift 0.8}}, -url = {http://arxiv.org/abs/1410.1881}, -volume = {800}, -year = {2015} -} -@article{Suyu2016, -abstract = {Strong gravitational lens systems with time delays between the multiple images allow measurements of time-delay distances, which are primarily sensitive to the Hubble constant that is key to probing dark energy, neutrino physics and the spatial curvature of the Universe, as well as discovering new physics. We present H0LiCOW (H0 Lenses in COSMOGRAIL's Wellspring), a program that aims to measure H0 with {\textless} 3.5 per cent uncertainty from five lens systems (B1608+656, RXJ1131-1231, HE 0435-1223, WFI2033-4723 and HE 1104-1805). We have been acquiring (1) time delays through COSMOGRAIL and Very Large Array2D monitoring, (2) high-resolution Hubble Space Telescope imaging for the lens mass modelling, (3) wide-field imaging and spectroscopy to characterize the lens environment and (4) moderate-resolution spectroscopy to obtain the stellar velocity dispersion of the lenses for mass modelling. In cosmological models with one-parameter extension to flat $\Lambda$ cold dark matter, we expect to measure H0 to {\textless} 3.5 per cent in most models, spatial curvature $\Omega$k to 0.004, w to 0.14 and the effective number of neutrino species to 0.2 (1$\sigma$ uncertainties) when combined with current cosmic microwave background (CMB) experiments. These are, respectively, a factor of {\~{}}15, {\~{}}2 and {\~{}}1.5 tighter than CMB alone. Our data set will further enable us to study the stellar initial mass function of the lens galaxies, and the co-evolution of supermassive black holes and their host galaxies. This program will provide a foundation for extracting cosmological distances from the hundreds of time-delay lenses that are expected to be discovered in current and future surveys.}, -archivePrefix = {arXiv}, -arxivId = {1607.00017}, -author = {Suyu, S. H. and Bonvin, V. and Courbin, F. and Fassnacht, C. D. and Rusu, C. E. and Sluse, D. and Treu, T. and Wong, K. C. and Auger, M. W. and Ding, X. and Hilbert, S. and Marshall, P. J. and Rumbaugh, N. and Sonnenfeld, A. and Tewes, M. and Tihhonova, O. and Agnello, A. and Blandford, R. D. and Chen, G. C.F. and Collett, T. and Koopmans, L. V.E. and Liao, K. and Meylan, G. and Spiniello, C.}, -doi = {10.1093/mnras/stx483}, -eprint = {1607.00017}, -file = {:home/jammy/.local/share/data/Mendeley Ltd./Mendeley Desktop/Downloaded/Suyu et al. - 2017 - H0LiCOW - I. H0 Lenses in COSMOGRAIL's Wellspring Program overview.pdf:pdf}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {Cosmological parameters,Distance scale,galaxies: structure,Gravitational lensing: strong,HE 0435-1223,HE 1104-1805,Quasars: individual:B1608+656,RXJ1131-1231,WFI2033-4723}, -number = {3}, -pages = {2590--2604}, -title = {{H0LiCOW - I. H0 Lenses in COSMOGRAIL's Wellspring: Program overview}}, -url = {http://arxiv.org/abs/1607.00017{\%}0Ahttp://dx.doi.org/10.1093/mnras/stx483}, -volume = {468}, -year = {2017} -} -@article{Treu2009, -abstract = {We study the relation between the internal structure of early-type galaxies and their environment using 70 strong gravitational lenses from the SLACS Survey. The Sloan Digital Sky Survey (SDSS) database is used to determine two measures of overdensity of galaxies around each lens - the projected number density of galaxies inside the tenth nearest neighbor ($\Sigma$10) and within a cone of radius one h -1 Mpc (D 1). Our main results are as follows. (1) The average overdensity is somewhat larger than unity, consistent with lenses preferring overdense environments as expected for massive early-type galaxies (12/70 lenses are in known groups/clusters). (2) The distribution of overdensities is indistinguishable from that of "twin" nonlens galaxies selected from SDSS to have the same redshift and stellar velocity dispersion $\sigma$*. Thus, within our errors, lens galaxies are an unbiased population, and the SLACS results can be generalized to the overall population of early-type galaxies. (3) Typical contributions from external mass distribution are no more than a few percent in local mass density, reaching 10-20{\%} (0.05-0.10 external convergence) only in the most extreme overdensities. (4) No significant correlation between overdensity and slope of the mass-density profile of the lens galaxies is found. (5) Satellite galaxies (those with a more luminous companion) have marginally steeper mass-density profiles (as quantified by f SIE = $\sigma$*/$\sigma$SIE = 1.12 ± 0.05 versus 1.01 ± 0.01) and smaller dynamically normalized mass enclosed within the Einstein radius ($\Delta$log M Ein/M dim differs by -0.09 ± 0.03 dex) than central galaxies (those without). This result suggests that tidal stripping may affect the mass structure of early-type galaxies down to kpc scales probed by strong lensing, when they fall into larger structures. {\textcopyright} 2009. The American Astronomical Society. All rights reserved.}, -archivePrefix = {arXiv}, -arxivId = {0806.1056}, -author = {Treu, Tommaso and Gavazzi, Rapha{\"{e}}l and Gorecki, Alexia and Marshall, Philip J. and Koopmans, L{\'{e}}on V.E. and Bolton, Adam S. and Moustakas, Leonidas A. and Burles, Scott}, -doi = {10.1088/0004-637X/690/1/670}, -eprint = {0806.1056}, -issn = {15384357}, -journal = {Astrophysical Journal}, -keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,galaxies: formation,galaxies: structure,Gravitational lensing}, -number = {1}, -pages = {670--682}, -title = {{The slacs survey. VIII. the relation between environment and internal structure of early-type galaxies}}, -url = {http://stacks.iop.org/0004-637X/690/i=1/a=670?key=crossref.f6cfae6390f195584737e04966a8c778}, -volume = {690}, -year = {2009} -} -@article{Vegetti2009, -abstract = {We introduce a new adaptive and fully Bayesian grid-based method to model strong gravitational lenses with extended images. The primary goal of this method is to quantify the level of luminous and dark mass substructure in massive galaxies, through their effect on highly magnified arcs and Einstein rings. The method is adaptive on the source plane, where a Delaunay tessellation is defined according to the lens mapping of a regular grid on to the source plane. The Bayesian penalty function allows us to recover the best non-linear potential-model parameters and/or a grid-based potential correction and to objectively quantify the level of regularization for both the source and potential. In addition, we implement a Nested-Sampling technique to quantify the errors on all non-linear mass model parameters - marginalized over all source and regularization parameters - and allow an objective ranking of different potential models in terms of the marginalized evidence. In particular, we are interested in comparing very smooth lens mass models with ones that contain mass substructures. The algorithm has been tested on a range of simulated data sets, created from a model of a realistic lens system. One of the lens systems is characterized by a smooth potential with a power-law density profile, 12 include a Navarro, Frenk and White (NFW) dark matter substructure of different masses and at different positions and one contains two NFW dark substructures with the same mass but with different positions. Reconstruction of the source and lens potential for all of these systems shows the method is able, in a realistic scenario, to identify perturbations with masses ≳107 M ⊙ when located on the Einstein ring. For positions both inside and outside of the ring, masses of at least 109 M⊙ are required (i.e. roughly the Einstein ring of the perturber needs to overlap with that of the main lens). Our method provides a fully novel and objective test of mass substructure in massive galaxies. {\textcopyright} 2008 RAS.}, -archivePrefix = {arXiv}, -arxivId = {0805.0201}, -author = {Vegetti, S. and Koopmans, L. V.E.}, -doi = {10.1111/j.1365-2966.2008.14005.x}, -eprint = {0805.0201}, -isbn = {0892-0915 (Print)$\backslash$r0892-0915 (Linking)}, -issn = {00358711}, -journal = {MNRAS}, -keywords = {Dark matter,galaxies: haloes,galaxies: structure,Gravitational lensing}, -month = {jan}, -number = {3}, -pages = {945--963}, -pmid = {20829068}, -title = {{Bayesian strong gravitational-lens modelling on adaptive grids: Objective detection of mass substructure in galaxies}}, -volume = {392}, -year = {2009} -} -@article{Vegetti2014, -abstract = {We present the results of a search for galaxy substructures in a sample of 11 gravitational lens galaxies from the Sloan Lens ACS Survey by Bolton et al. We find no significant detection of mass clumps, except for a luminous satellite in the system SDSS J0956+5110. We use these non-detections, in combination with a previous detection in the system SDSS J0946+1006, to derive constraints on the substructure mass function in massive early-type host galaxies with an average redshift zlens {\~{}}0.2 and an average velocity dispersion seff {\~{}}270 km s-1. We perform a Bayesian inference on the substructure mass function, within a median region of about 32 kpc2 around the Einstein radius (Rein {\~{}}4.2 kpc). We infer a mean projected substructure mass fraction f = 0.0076+0.0208-0.0052 at the 68 per cent confidence level and a substructure mass function slopea {\textless} 2.93 at the 95 per cent confidence level for a uniform prior probability density on a. For a Gaussian prior based on cold dark matter (CDM) simulations, we infer f = 0.0064+0.0080-0.0042 and a slope of a =1.90+0.098-0.098 at the 68 per cent confidence level. Since only one substructure was detected in the full sample, we have little information on the mass function slope, which is therefore poorly constrained (i.e. the Bayes factor shows no positive preference for any of the two models). The inferred fraction is consistent with the expectations from CDM simulations and with inference from flux ratio anomalies at the 68 per cent confidence level. {\textcopyright} 2014 The Authors Published by Oxford University Press on behalf of the Royal Astronomical Society.}, -archivePrefix = {arXiv}, -arxivId = {1405.3666}, -author = {Vegetti, S. and Koopmans, L. V.E. and Auger, M. W. and Treu, T. and Bolton, A. S.}, -doi = {10.1093/mnras/stu943}, -eprint = {1405.3666}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {galaxies,Structure}, -month = {aug}, -number = {3}, -pages = {2017--2035}, -title = {{Inference of the cold dark matter substructure mass function at z = 0.2 using strong gravitational lenses}}, -volume = {442}, -year = {2014} -} -@article{Atek2015, -abstract = {Exploiting the power of gravitational lensing, the Hubble Frontier Fields (HFF) program aims at observing six massive galaxy clusters to explore the distant universe far beyond the limits of blank field surveys. Using the complete Hubble Space Telescope observations of the first HFF cluster A2744, we report the detection of 50 galaxy candidates at z ∼ 7 and eight candidates at z ∼ 8 in a total survey area of 0.96 arcmin2 in the source plane. Three of these galaxies are multiply imaged by the lensing cluster. Using an updated model of the mass distribution in the cluster we were able to calculate the magnification factor and the effective survey volume for each galaxy in order to compute the ultraviolet galaxy luminosity function (LF) at both redshifts 7 and 8. Our new measurements reliably extend the z ∼ 7 UV LF down to an absolute magnitude of MUV ∼ -15.5. We find a characteristic magnitude of M∗UV = -20.90-0.73+0.90 mag and a faint-end slope $\alpha$ = -2.01-0.28+0.20,close to previous determinations in blank fields. We show here for the first time that this slope remains steep down to very faint luminosities of 0.01 L∗. Although prone to large uncertainties, our results at z ∼ 8 also seem to confirm a steep faint-end slope below 0.1 L∗. The HFF program is therefore providing an extremely efficient way to study the faintest galaxy populations at z {\textgreater} 7 that would otherwise be inaccessible with current instrumentation. The full sample of six galaxy clusters will provide even better constraints on the buildup of galaxies at early epochs and their contribution to cosmic reionization.}, -archivePrefix = {arXiv}, -arxivId = {1409.0512}, -author = {Atek, Hakim and Richard, Johan and Kneib, Jean Paul and Jauzac, Mathilde and Schaerer, Daniel and Clement, Benjamin and Limousin, Marceau and Jullo, Eric and Natarajan, Priyamvada and Egami, Eiichi and Ebeling, Harald}, -doi = {10.1088/0004-637X/800/1/18}, -eprint = {1409.0512}, -file = {:home/jammy/Documents/Papers/StrongLensCluster/Atek2015HFFUV.pdf:pdf}, -issn = {15384357}, -journal = {Astrophysical Journal}, -keywords = {galaxies: evolution,galaxies: high-redshift,galaxies: luminosity function, mass function,Gravitational lensing: strong}, -number = {1}, -title = {{New constraints on the faint end of the UV luminosity function at z ∼ 7-8 using the gravitational lensing of the hubble frontier fields cluster A2744}}, -volume = {800}, -year = {2015} -} -@article{Jullo2010, -abstract = {Current efforts in observational cosmology are focused on characterizing the mass-energy content of the universe. We present results from a geometric test based on strong lensing in galaxy clusters. Based on Hubble Space Telescope images and extensive ground-based spectroscopic follow-up of the massive galaxy cluster Abell 1689, we used a parametric model to simultaneously constrain the cluster mass distribution and dark energy equation of state. Combining our cosmological constraints with those from x-ray clusters and the Wilkinson Microwave Anisotropy Probe 5-year data gives $\Omega$m = 0.25 ±0.05 and Wx = -0.97 ±0.07, which are consistent with results from other methods. Inclusion of our method with all other available techniques brings down the current 2$\sigma$ contours on the dark energy equation-of-state parameter wx by ∼30{\%}.}, -archivePrefix = {arXiv}, -arxivId = {1008.4802}, -author = {Jullo, Eric and Natarajan, Priyamvada and Kneib, Jean Paul and D'Aloisio, Anson and Limousin, Marceau and Richard, Johan and Schimd, Carlo}, -doi = {10.1126/science.1185759}, -eprint = {1008.4802}, -file = {:home/jammy/Documents/Papers/StrongLensCluster/Jullo2008CosmologicalConstraints.pdf:pdf}, -issn = {00368075}, -journal = {Science}, -number = {5994}, -pages = {924--927}, -title = {{Cosmological constraints from strong gravitational lensing in clusters of galaxies}}, -volume = {329}, -year = {2010} -} -@article{Richard2014, -abstract = {Extending over three Hubble Space Telescope (HST) cycles, the Hubble Frontier Fields (HFF) initiative constitutes the largest commitment ever of HST time to the exploration of the distant Universe via gravitational lensing by massive galaxy clusters. Here, we present models of the mass distribution in the six HFF cluster lenses, derived from a joint strong- and weak-lensing analysis anchored by a total of 88 multiple-image systems identified in existing HST data. The resulting maps of the projected mass distribution and of the gravitational magnification effectively calibrate the HFF clusters as gravitational telescopes. Allowing the computation of search areas in the source plane, these maps are provided to the community to facilitate the exploitation of forthcoming HFF data for quantitative studies of the gravitationally lensed population of background galaxies. Our models of the gravitational magnification afforded by the HFF clusters allow us to quantify the lensing-induced boost in sensitivity over blank-field observations and predict that galaxies at z {\textgreater} 10 and as faint as m(AB) = 32 will be detectable, up to 2 mag fainter than the limit of the Hubble Ultra Deep Field.}, -author = {Richard, Johan and Jauzac, Mathilde and Limousin, Marceau and Jullo, Eric and Cl{\'{e}}ment, Benjamin and Ebeling, Harald and Kneib, Jean Paul and Atek, Hakim and Natarajan, Priya and Egami, Eiichi and Livermore, Rachael and Bower, Richard}, -doi = {10.1093/mnras/stu1395}, -file = {:home/jammy/Documents/Papers/StrongLensCluster/Richard2017HFFModels.pdf:pdf}, -issn = {13652966}, -journal = {MNRAS}, -keywords = {galaxies clusters: individual: A1063S,galaxies clusters: individual: Abell 370,galaxies clusters: individual: MACS J0416.1-2403,galaxies clusters: individual: MACS J0717.5+3745,galaxies clusters: individual: MACS J1149.5+2223,galaxies clusters: individual: Abell 2744}, -number = {1}, -pages = {268--289}, -title = {{Mass and magnification maps for the hubble space telescope frontier fields clusters: Implications for high-redshift studies}}, -volume = {444}, -year = {2014} -} -@article{Powell2020, - author = {Powell, Devon and Vegetti, Simona and McKean, John P and Spingola, Cristiana and Rizzo, Francesca and Stacey, Hannah R}, - title = "{A novel approach to visibility-space modelling of interferometric gravitational lens observations at high angular resolution}", - journal = {MNRAS}, - volume = {501}, - number = {1}, - pages = {515-530}, - year = {2020}, - month = {09}, - abstract = "{We present a new gravitational lens modelling technique designed to model high-resolution interferometric observations with large numbers of visibilities without the need to pre-average the data in time or frequency. We demonstrate the accuracy of the method using validation tests on mock observations. Using small data sets with ∼103 visibilities, we first compare our approach with the more traditional direct Fourier transform (DFT) implementation and direct linear solver. Our tests indicate that our source inversion is indistinguishable from that of the DFT. Our method also infers lens parameters to within 1 to 2 per cent of both the ground truth and DFT, given sufficiently high signal-to-noise ratio (SNR). When the SNR is as low as 5, both approaches lead to errors of several tens of per cent in the lens parameters and a severely disrupted source structure, indicating that this is related to the SNR and choice of priors rather than the modelling technique itself. We then analyse a large data set with ∼108 visibilities and a SNR matching real global Very Long Baseline Interferometry observations of the gravitational lens system MG J0751+2716. The size of the data is such that it cannot be modelled with traditional implementations. Using our novel technique, we find that we can infer the lens parameters and the source brightness distribution, respectively, with an RMS error of 0.25 and 0.97 per cent relative to the ground truth.}", - issn = {0035-8711}, - doi = {10.1093/mnras/staa2740}, - url = {https://doi.org/10.1093/mnras/staa2740}, - eprint = {https://academic.oup.com/mnras/article-pdf/501/1/515/35069118/staa2740.pdf}, -} -@article{Birrer2018a, -abstract = {We present lenstronomy, a multi-purpose open-source gravitational lens modelling pythonpackage. lenstronomy is able to reconstruct the lens mass and surface brightness distributions of strong lensing systems using forward modelling. lenstronomy supports a wide range of analytic lens and light models in arbitrary combination. The software is also able to reconstruct complex extended sources (Birrer et. al 2015) as well as being able to model point sources. We designed lenstronomy to be stable, flexible and numerically accurate, with a clear user interface that could be deployed across different platforms. Throughout its development, we have actively used lenstronomy to make several measurements including deriving constraints on dark matter properties in strong lenses, measuring the expansion history of the universe with time-delay cosmography, measuring cosmic shear with Einstein rings and decomposing quasar and host galaxy light. The software is distributed under the MIT license. The documentation, starter guide, example notebooks, source code and installation guidelines can be found at https://lenstronomy.readthedocs.io.}, -archivePrefix = {arXiv}, -arxivId = {1803.09746}, -author = {Birrer, Simon and Amara, Adam}, -doi = {10.1016/j.dark.2018.11.002}, -eprint = {1803.09746}, -file = {:home/jammy/Documents/Papers/Strong{\_}Lens/Biirrer2018lenstronomy.pdf:pdf}, -issn = {22126864}, -journal = {Physics of the Dark Universe}, -keywords = {Gravitational lensing,Image simulations,Software}, -pages = {189--201}, -title = {{lenstronomy: Multi-purpose gravitational lens modelling software package}}, -volume = {22}, -year = {2018} -} -@ARTICLE{spilker16a, - author = {{Spilker}, J.~S. and {Marrone}, D.~P. and {Aravena}, M. and - {B{\'e}thermin}, M. and {Bothwell}, M.~S. and {Carlstrom}, J.~E. and - {Chapman}, S.~C. and {Crawford}, T.~M. and {de Breuck}, C. and - {Fassnacht}, C.~D. and {Gonzalez}, A.~H. and {Greve}, T.~R. and - {Hezaveh}, Y. and {Litke}, K. and {Ma}, J. and {Malkan}, M. and - {Rotermund}, K.~M. and {Strandet}, M. and {Vieira}, J.~D. and - {Weiss}, A. and {Welikala}, N.}, - title = "{ALMA Imaging and Gravitational Lens Models of South Pole Telescope{\mdash}Selected Dusty, Star-Forming galaxies at High Redshifts}", - journal = {\apj}, - archivePrefix = "arXiv", - eprint = {1604.05723}, - keywords = {galaxies: high-redshift, galaxies: ISM, galaxies: star formation }, - year = 2016, - month = aug, - volume = 826, - eid = {112}, - pages = {112}, - doi = {10.3847/0004-637X/826/2/112}, - adsurl = {http://adsabs.harvard.edu/abs/2016ApJ...826..112S}, - adsnote = {Provided by the SAO/NASA Astrophysics Data System} -} +@article{astropy1, +Adsnote = {Provided by the SAO/NASA Astrophysics Data System}, +Adsurl = {http://adsabs.harvard.edu/abs/2013A%26A...558A..33A}, +Archiveprefix = {arXiv}, +Author = {{Astropy Collaboration} and {Robitaille}, T.~P. and {Tollerud}, E.~J. and {Greenfield}, P. and {Droettboom}, M. and {Bray}, E. and {Aldcroft}, T. and {Davis}, M. and {Ginsburg}, A. and {Price-Whelan}, A.~M. and {Kerzendorf}, W.~E. and {Conley}, A. and {Crighton}, N. and {Barbary}, K. and {Muna}, D. and {Ferguson}, H. and {Grollier}, F. and {Parikh}, M.~M. and {Nair}, P.~H. and {Unther}, H.~M. and {Deil}, C. and {Woillez}, J. and {Conseil}, S. and {Kramer}, R. and {Turner}, J.~E.~H. and {Singer}, L. and {Fox}, R. and {Weaver}, B.~A. and {Zabalza}, V. and {Edwards}, Z.~I. and {Azalee Bostroem}, K. and {Burke}, D.~J. and {Casey}, A.~R. and {Crawford}, S.~M. and {Dencheva}, N. and {Ely}, J. and {Jenness}, T. and {Labrie}, K. and {Lim}, P.~L. and {Pierfederici}, F. and {Pontzen}, A. and {Ptak}, A. and {Refsdal}, B. and {Servillat}, M. and {Streicher}, O.}, +Doi = {10.1051/0004-6361/201322068}, +Eid = {A33}, +Eprint = {1307.6212}, +Journal = {\aap}, +Keywords = {methods: data analysis, methods: miscellaneous, virtual observatory tools}, +Month = oct, +Pages = {A33}, +Primaryclass = {astro-ph.IM}, +Title = {{Astropy: A community Python package for astronomy}}, +Volume = 558, +Year = 2013, +Bdsk-Url-1 = {https://dx.doi.org/10.1051/0004-6361/201322068}} +@article{astropy2, +Adsnote = {Provided by the SAO/NASA Astrophysics Data System}, +Adsurl = {https://ui.adsabs.harvard.edu/#abs/2018AJ....156..123T}, +Author = {{Price-Whelan}, A.~M. and {Sip{\H{o}}cz}, B.~M. and {G{\"u}nther}, H.~M. and {Lim}, P.~L. and {Crawford}, S.~M. and {Conseil}, S. and {Shupe}, D.~L. and {Craig}, M.~W. and {Dencheva}, N. and {Ginsburg}, A. and {VanderPlas}, J.~T. and {Bradley}, L.~D. and {P{\'e}rez-Su{\'a}rez}, D. and {de Val-Borro}, M. and {Paper Contributors}, (Primary and {Aldcroft}, T.~L. and {Cruz}, K.~L. and {Robitaille}, T.~P. and {Tollerud}, E.~J. and {Coordination Committee}, (Astropy and {Ardelean}, C. and {Babej}, T. and {Bach}, Y.~P. and {Bachetti}, M. and {Bakanov}, A.~V. and {Bamford}, S.~P. and {Barentsen}, G. and {Barmby}, P. and {Baumbach}, A. and {Berry}, K.~L. and {Biscani}, F. and {Boquien}, M. and {Bostroem}, K.~A. and {Bouma}, L.~G. and {Brammer}, G.~B. and {Bray}, E.~M. and {Breytenbach}, H. and {Buddelmeijer}, H. and {Burke}, D.~J. and {Calderone}, G. and {Cano Rodr{\'\i}guez}, J.~L. and {Cara}, M. and {Cardoso}, J.~V.~M. and {Cheedella}, S. and {Copin}, Y. and {Corrales}, L. and {Crichton}, D. and {D{\textquoteright}Avella}, D. and {Deil}, C. and {Depagne}, {\'E}. and {Dietrich}, J.~P. and {Donath}, A. and {Droettboom}, M. and {Earl}, N. and {Erben}, T. and {Fabbro}, S. and {Ferreira}, L.~A. and {Finethy}, T. and {Fox}, R.~T. and {Garrison}, L.~H. and {Gibbons}, S.~L.~J. and {Goldstein}, D.~A. and {Gommers}, R. and {Greco}, J.~P. and {Greenfield}, P. and {Groener}, A.~M. and {Grollier}, F. and {Hagen}, A. and {Hirst}, P. and {Homeier}, D. and {Horton}, A.~J. and {Hosseinzadeh}, G. and {Hu}, L. and {Hunkeler}, J.~S. and {Ivezi{\'c}}, {\v{Z}}. and {Jain}, A. and {Jenness}, T. and {Kanarek}, G. and {Kendrew}, S. and {Kern}, N.~S. and {Kerzendorf}, W.~E. and {Khvalko}, A. and {King}, J. and {Kirkby}, D. and {Kulkarni}, A.~M. and {Kumar}, A. and {Lee}, A. and {Lenz}, D. and {Littlefair}, S.~P. and {Ma}, Z. and {Macleod}, D.~M. and {Mastropietro}, M. and {McCully}, C. and {Montagnac}, S. and {Morris}, B.~M. and {Mueller}, M. and {Mumford}, S.~J. and {Muna}, D. and {Murphy}, N.~A. and {Nelson}, S. and {Nguyen}, G.~H. and {Ninan}, J.~P. and {N{\"o}the}, M. and {Ogaz}, S. and {Oh}, S. and {Parejko}, J.~K. and {Parley}, N. and {Pascual}, S. and {Patil}, R. and {Patil}, A.~A. and {Plunkett}, A.~L. and {Prochaska}, J.~X. and {Rastogi}, T. and {Reddy Janga}, V. and {Sabater}, J. and {Sakurikar}, P. and {Seifert}, M. and {Sherbert}, L.~E. and {Sherwood-Taylor}, H. and {Shih}, A.~Y. and {Sick}, J. and {Silbiger}, M.~T. and {Singanamalla}, S. and {Singer}, L.~P. and {Sladen}, P.~H. and {Sooley}, K.~A. and {Sornarajah}, S. and {Streicher}, O. and {Teuben}, P. and {Thomas}, S.~W. and {Tremblay}, G.~R. and {Turner}, J.~E.~H. and {Terr{\'o}n}, V. and {van Kerkwijk}, M.~H. and {de la Vega}, A. and {Watkins}, L.~L. and {Weaver}, B.~A. and {Whitmore}, J.~B. and {Woillez}, J. and {Zabalza}, V. and {Contributors}, (Astropy}, +Doi = {10.3847/1538-3881/aabc4f}, +Eid = {123}, +Journal = {\aj}, +Keywords = {methods: data analysis, methods: miscellaneous, methods: statistical, reference systems, Astrophysics - Instrumentation and Methods for Astrophysics}, +Month = Sep, +Pages = {123}, +Primaryclass = {astro-ph.IM}, +Title = {{The Astropy Project: Building an Open-science Project and Status of the v2.0 Core Package}}, +Volume = {156}, +Year = 2018, +Bdsk-Url-1 = {https://doi.org/10.3847/1538-3881/aabc4f}} + +@article{colossus, +abstract = {This paper introduces Colossus, a public, open-source python package for calculations related to cosmology, the large-scale structure (LSS) of matter in the universe, and the properties of dark matter halos. The code is designed to be fast and easy to use, with a coherent, well-documented user interface. The cosmology module implements Friedman-Lemaitre-Robertson-Walker cosmologies including curvature, relativistic species, and different dark energy equations of state, and provides fast computations of the linear matter power spectrum, variance, and correlation function. The LSS module is concerned with the properties of peaks in Gaussian random fields and halos in a statistical sense, including their peak height, peak curvature, halo bias, and mass function. The halo module deals with spherical overdensity radii and masses, density profiles, concentration, and the splashback radius. To facilitate the rapid exploration of these quantities, Colossus implements more than 40 different fitting functions from the literature. I discuss the core routines in detail, with particular emphasis on their accuracy. Colossus is available at bitbucket.org/bdiemer/colossus.}, +archivePrefix = {arXiv}, +arxivId = {1712.04512}, +author = {Diemer, Benedikt}, +doi = {10.3847/1538-4365/aaee8c}, +eprint = {1712.04512}, +file = {:home/jammy/Documents/Papers/Software/Collosus2018.pdf:pdf}, +issn = {0067-0049}, +journal = {The Astrophysical Journal Supplement Series}, +keywords = {cosmology,cosmology: theory,methods: numerical,methods,numerical,theory}, +number = {2}, +pages = {35}, +publisher = {IOP Publishing}, +title = {{COLOSSUS: A Python Toolkit for Cosmology, Large-scale Structure, and Dark Matter Halos}}, +url = {http://dx.doi.org/10.3847/1538-4365/aaee8c}, +volume = {239}, +year = {2018} +} +@article{corner, + doi = {10.21105/joss.00024}, + url = {https://doi.org/10.21105/joss.00024}, + year = {2016}, + month = {jun}, + publisher = {The Open Journal}, + volume = {1}, + number = {2}, + pages = {24}, + author = {Daniel Foreman-Mackey}, + title = {corner.py: Scatterplot matrices in Python}, + journal = {The Journal of Open Source Software} +} +@article{dynesty, +abstract = {We present dynesty, a public, open-source, python package to estimate Bayesian posteriors and evidences (marginal likelihoods) using the dynamic nested sampling methods developed by Higson et al. By adaptively allocating samples based on posterior structure, dynamic nested sampling has the benefits of Markov chain Monte Carlo (MCMC) algorithms that focus exclusively on posterior estimation while retaining nested sampling's ability to estimate evidences and sample from complex, multimodal distributions. We provide an overview of nested sampling, its extension to dynamic nested sampling, the algorithmic challenges involved, and the various approaches taken to solve them in this and previous work. We then examine dynesty's performance on a variety of toy problems along with several astronomical applications. We find in particular problems dynesty can provide substantial improvements in sampling efficiency compared to popular MCMC approaches in the astronomical literature. More detailed statistical results related to nested sampling are also included in the appendix.}, +archivePrefix = {arXiv}, +arxivId = {1904.02180}, +author = {Speagle, Joshua S}, +doi = {10.1093/mnras/staa278}, +eprint = {1904.02180}, +file = {:home/jammy/Documents/Papers/PPLs/Dynesty.pdf:pdf}, +issn = {0035-8711}, +journal = {MNRAS}, +keywords = {data analysis,methods,statistical}, +number = {3}, +pages = {3132--3158}, +title = {{dynesty: a dynamic nested sampling package for estimating Bayesian posteriors and evidences}}, +volume = {493}, +year = {2020} +} +@article{emcee, +abstract = {We introduce a stable, well tested Python implementation of the affine-invariant ensemble sampler for Markov chain Monte Carlo (MCMC) proposed by Goodman {\&} Weare (2010). The code is open source and has already been used in several published projects in the astrophysics literature. The algorithm behind emcee has several advantages over traditional MCMC sampling methods and it has excellent performance as measured by the autocorrelation time (or function calls per independent sample). One major advantage of the algorithm is that it requires hand-tuning of only 1 or 2 parameters compared to {\$}\backslashsim N{\^{}}2{\$} for a traditional algorithm in an N-dimensional parameter space. In this document, we describe the algorithm and the details of our implementation and API. Exploiting the parallelism of the ensemble method, emcee permits any user to take advantage of multiple CPU cores without extra effort. The code is available online at http://dan.iel.fm/emcee under the MIT License.}, +archivePrefix = {arXiv}, +arxivId = {1202.3665}, +author = {Foreman-Mackey, Daniel and Hogg, David W. and Lang, Dustin and Goodman, Jonathan}, +doi = {10.1086/670067}, +eprint = {1202.3665}, +file = {:home/jammy/Documents/Papers/PPLs/Emcee.pdf:pdf}, +issn = {00046280}, +journal = {Publications of the Astronomical Society of the Pacific}, +number = {925}, +pages = {306--312}, +title = {{emcee : The MCMC Hammer}}, +volume = {125}, +year = {2013} +} +@article{matplotlib, + Author = {Hunter, J. D.}, + Title = {Matplotlib: A 2D graphics environment}, + Journal = {Computing in Science \& Engineering}, + Volume = {9}, + Number = {3}, + Pages = {90--95}, + abstract = {Matplotlib is a 2D graphics package used for Python for + application development, interactive scripting, and publication-quality + image generation across user interfaces and operating systems.}, + publisher = {IEEE COMPUTER SOC}, + doi = {10.1109/MCSE.2007.55}, + year = 2007 +} +@article{numba, +abstract = {Dynamic, interpreted languages, like Python, are attractive for domain-experts and scientists experimenting with new ideas. However, the performance of the interpreter is of-ten a barrier when scaling to larger data sets. This paper presents a just-in-time compiler for Python that focuses in scientific and array-oriented computing. Starting with the simple syntax of Python, Numba compiles a subset of the language into efficient machine code that is comparable in performance to a traditional compiled language. In addi-tion, we share our experience in building a JIT compiler using LLVM[1].}, +author = {Lam, Siu Kwan and Pitrou, Antoine and Seibert, Stanley}, +doi = {10.1145/2833157.2833162}, +file = {:home/jammy/Documents/Papers/Software/numba{\_}sc15.pdf:pdf}, +isbn = {9781450340052}, +journal = {Proceedings of the Second Workshop on the LLVM Compiler Infrastructure in HPC - LLVM '15}, +keywords = {2,a jit for numeric,com-,compiler,jit,just-in-time,llvm,numba is a function-at-a-time,python}, +pages = {1--6}, +title = {{Numba: a LLVM-based Python JIT compiler}}, +url = {http://dl.acm.org/citation.cfm?doid=2833157.2833162}, +year = {2015} +} +@article{numpy, + author={S. {van der Walt} and S. C. {Colbert} and G. {Varoquaux}}, + journal={Computing in Science Engineering}, + title={The NumPy Array2D: A Structure for Efficient Numerical Computation}, + year={2011}, + volume={13}, + number={2}, + pages={22-30}, + doi={10.1109/MCSE.2011.37}} + +@article{pyautofit, + doi = {10.21105/joss.02550}, + url = {https://doi.org/10.21105/joss.02550}, + year = {2021}, + publisher = {The Open Journal}, + volume = {6}, + number = {58}, + pages = {2550}, + author = {Nightingale, J. W. and Hayes, R. G. and Griffiths, M.}, + title = {`PyAutoFit`: A Classy Probabilistic Programming Language for Model Composition and Fitting}, + journal = {J. Open Source Softw.} +} +@article{multinest, +abstract = {We present further development and the first public release of our multimodal nested sampling algorithm, called MultiNest. This Bayesian inference tool calculates the evidence, with an associated error estimate, and produces posterior samples from distributions that may contain multiple modes and pronounced (curving) degeneracies in high dimensions. The developments presented here lead to further substantial improvements in sampling efficiency and robustness, as compared to the original algorithm presented in Feroz and Hobson, which itself significantly outperformed existing Markov chain Monte Carlo techniques in a wide range of astrophysical inference problems. The accuracy and economy of the MultiNest algorithm are demonstrated by application to two toy problems and to a cosmological inference problem focusing on the extension of the vanilla $\Lambda$ cold dark matter model to include spatial curvature and a varying equation of state for dark energy. The MultiNest software, which is fully parallelized using MPI and includes an interface to CosmoMC, is available at http://www.mrao.cam.ac.uk/software/multinest/. It will also be released as part of the SuperBayeS package, for the analysis of supersymmetric theories of particle physics, at http://www.superbayes.org. {\textcopyright} 2009 RAS.}, +archivePrefix = {arXiv}, +arxivId = {0809.3437}, +author = {Feroz, F. and Hobson, M. P. and Bridges, M.}, +doi = {10.1111/j.1365-2966.2009.14548.x}, +eprint = {0809.3437}, +isbn = {0035-8711}, +issn = {00358711}, +journal = {MNRAS}, +keywords = {Methods: Data analysis,Methods: Statistical}, +number = {4}, +pages = {1601--1614}, +pmid = {29176}, +title = {{MultiNest: An efficient and robust Bayesian inference tool for cosmology and particle physics}}, +volume = {398}, +year = {2009} +} +@article{pymultinest, +abstract = {Context. Aims. Active galactic nuclei are known to have complex X-ray spectra that depend on both the properties of the accrediting super-massive black hole (e.g. mass, accretion rate) and the distribution of obscuring material in its vicinity (i.e. the "torus"). Often however, simple and even unphysical models are adopted to represent the X-ray spectra of AGN, which do not capture the complexity and diversity of the observations. In the case of blank field surveys in particular, this should have an impact on e.g. the determination of the AGN luminosity function, the inferred accretion history of the Universe and also on our understanding of the relation between AGN and their host galaxies. Methods. We develop a Bayesian framework for model comparison and parameter estimation of X-ray spectra. We take into account uncertainties associated with both the Poisson nature of X-ray data and the determination of source redshift using photometric methods. We also demonstrate how Bayesian model comparison can be used to select among ten different physically motivated X-ray spectral models the one that provides a better representation of the observations. This methodology is applied to X-ray AGN in the 4 Ms Chandra Deep Field South. Results. For the {\~{}}350 AGN in that field, our analysis identifies four components needed to represent the diversity of the observed X-ray spectra: (1) an intrinsic power law; (2) a cold obscurer which reprocesses the radiation due to photo-electric absorption, Compton scattering and Fe-K fluorescence; (3) an unabsorbed power law associated with Thomson scattering off ionised clouds; and (4) Compton reflection, most noticeable from a stronger-than-expected Fe-K line. Simpler models, such as a photo-electrically absorbed power law with a Thomson scattering component, are ruled out with decisive evidence (B {\textgreater} 100). We also find that ignoring the Thomson scattering component results in underestimation of the inferred column density, NH, of the obscurer. Regarding the geometry of the obscurer, there is strong evidence against both a completely closed (e.g. sphere), or entirely open (e.g. blob of material along the line of sight), toroidal geometry in favour of an intermediate case. Conclusions. Despite the use of low-count spectra, our methodology is able to draw strong inferences on the geometry of the torus. Simpler models are ruled out in favour of a geometrically extended structure with significant Compton scattering. We confirm the presence of a soft component, possibly associated with Thomson scattering off ionised clouds in the opening angle of the torus. The additional Compton reflection required by data over that predicted by toroidal geometry models, may be a sign of a density gradient in the torus or reflection off the accretion disk. Finally, we release a catalogue of AGN in the CDFS with estimated parameters such as the accretion luminosity in the 2-10 keV band and the column density, NH, of the obscurer. {\textcopyright} ESO, 2014.}, +archivePrefix = {arXiv}, +arxivId = {1402.0004}, +author = {Buchner, J. and Georgakakis, A. and Nandra, K. and Hsu, L. and Rangel, C. and Brightman, M. and Merloni, A. and Salvato, M. and Donley, J. and Kocevski, D.}, +doi = {10.1051/0004-6361/201322971}, +eprint = {1402.0004}, +file = {:home/jammy/Documents/Papers/Stats/ButchnerPyMultiNest.pdf:pdf}, +issn = {14320746}, +journal = {A&A}, +keywords = {Accretion, accretion disks,galaxies: high-redshift,galaxies: nuclei,Methods: data analysis,Methods: statistical,X-rays: galaxies}, +pages = {A125}, +title = {{X-ray spectral modelling of the AGN obscuring region in the CDFS: Bayesian model selection and catalogue}}, +volume = {564}, +year = {2014} +} +@article{pynufft, +abstract = {A Python non-uniform fast Fourier transform (PyNUFFT) package has been developed to accelerate multidimensional non-Cartesian image reconstruction on heterogeneous platforms. Since scientific computing with Python encompasses a mature and integrated environment, the time efficiency of the NUFFT algorithm has been a major obstacle to real-time non-Cartesian image reconstruction with Python. The current PyNUFFT software enables multi-dimensional NUFFT accelerated on a heterogeneous platform, which yields an efficient solution to many non-Cartesian imaging problems. The PyNUFFT also provides several solvers, including the conjugate gradient method, 1 total variation regularized ordinary least square (L1TV-OLS), and 1 total variation regularized least absolute deviation (L1TV-LAD). Metaprogramming libraries have been employed to accelerate PyNUFFT. The PyNUFFT package has been tested on multi-core central processing units (CPUs) and graphic processing units (GPUs), with acceleration factors of 6.3–9.5× on a 32-thread CPU platform and 5.4–13× on a GPU.}, +author = {Lin, Jyh Miin}, +doi = {10.3390/jimaging4030051}, +file = {:home/jammy/Documents/Papers/Software/jimaging-04-00051-v2.pdf:pdf}, +issn = {2313433X}, +journal = {Journal of Imaging}, +keywords = {Graphic processing unit (GPU),Heterogeneous system architecture (HSA),Magnetic resonance imaging (MRI),Multi-core system,Total variation (TV)}, +number = {3}, +pages = {1--22}, +title = {{Python non-uniform fast fourier transform (PyNUFFT): An accelerated non-cartesian MRI package on a heterogeneous platform (CPU/GPU)}}, +volume = {4}, +year = {2018} +} +@software{pyquad, + author = {Ashley J. Kelly}, + title = {pyquad}, + month = jul, + year = 2020, + publisher = {Zenodo}, + version = {0.6.4}, + doi = {10.5281/zenodo.3936959}, + url = {https://doi.org/10.5281/zenodo.3936959} +} +@article{pyswarms, + author = {Lester James V. Miranda}, + title = "{P}y{S}warms, a research-toolkit for {P}article {S}warm {O}ptimization in {P}ython", + journal = {J. Open Source Softw.}, + year = {2018}, + volume = {3}, + issue = {21}, + doi = {10.21105/joss.00433}, + url = {https://doi.org/10.21105/joss.00433} +} + @book{python, + author = {Van Rossum, Guido and Drake, Fred L.}, + title = {Python 3 Reference Manual}, + year = {2009}, + isbn = {1441412697}, + publisher = {CreateSpace}, + address = {Scotts Valley, CA} +} +@article{scikit-image, + title={scikit-image: image processing in Python}, + author={Van der Walt, Stefan and Sch{\"o}nberger, Johannes L and Nunez-Iglesias, Juan and Boulogne, Fran{\c{c}}ois and Warner, Joshua D and Yager, Neil and Gouillart, Emmanuelle and Yu, Tony}, + journal={PeerJ}, + volume={2}, + pages={e453}, + year={2014}, + publisher={PeerJ Inc.} +} +@article{scikit-learn, + title={Scikit-learn: Machine Learning in {P}ython}, + author={Pedregosa, F. and Varoquaux, G. and Gramfort, A. and Michel, V. + and Thirion, B. and Grisel, O. and Blondel, M. and Prettenhofer, P. + and Weiss, R. and Dubourg, V. and Vanderplas, J. and Passos, A. and + Cournapeau, D. and Brucher, M. and Perrot, M. and Duchesnay, E.}, + journal={Journal of Machine Learning Research}, + volume={12}, + pages={2825--2830}, + year={2011} +} +@article{scipy, + author = {{Virtanen}, Pauli and {Gommers}, Ralf and {Oliphant}, + Travis E. and {Haberland}, Matt and {Reddy}, Tyler and + {Cournapeau}, David and {Burovski}, Evgeni and {Peterson}, Pearu + and {Weckesser}, Warren and {Bright}, Jonathan and {van der Walt}, + St{\'e}fan J. and {Brett}, Matthew and {Wilson}, Joshua and + {Jarrod Millman}, K. and {Mayorov}, Nikolay and {Nelson}, Andrew + R.~J. and {Jones}, Eric and {Kern}, Robert and {Larson}, Eric and + {Carey}, CJ and {Polat}, {\.I}lhan and {Feng}, Yu and {Moore}, + Eric W. and {Vand erPlas}, Jake and {Laxalde}, Denis and + {Perktold}, Josef and {Cimrman}, Robert and {Henriksen}, Ian and + {Quintero}, E.~A. and {Harris}, Charles R and {Archibald}, Anne M. + and {Ribeiro}, Ant{\^o}nio H. and {Pedregosa}, Fabian and + {van Mulbregt}, Paul and {Contributors}, SciPy 1. 0}, + title = "{SciPy 1.0: Fundamental Algorithms for Scientific + Computing in Python}", + journal = {Nature Methods}, + year = "2020", + volume={17}, + pages={261--272}, + adsurl = {https://rdcu.be/b08Wh}, + doi = {10.1038/s41592-019-0686-2}, +} +@article{Alexander2019, +abstract = {Dark matter substructure has the potential to discriminate between broad classes of dark matter models. With this in mind, we construct novel solutions to the equations of motion governing condensate dark matter candidates, namely axion Bose-Einstein condensates and superfluids. These solutions are highly compressed along one axis and thus have a disk-like geometry. We discuss linear stability of these solutions, consider the astrophysical implications as a large-scale dark disk or as small scale substructure, and find a characteristic signal in strong gravitational lensing. This adds to the growing body of work that indicates that the morphology of dark matter substructure is a powerful probe of the nature of dark matter.}, +archivePrefix = {arXiv}, +arxivId = {1901.03694}, +author = {Alexander, Stephon and Bramburger, Jason J. and McDonough, Evan}, +doi = {10.1016/j.physletb.2019.134871}, +eprint = {1901.03694}, +file = {:home/jammy/Documents/Papers/PyAutoLens/Alexander2018DiskSuperfliod.pdf:pdf}, +issn = {03702693}, +journal = {Physics Letters, Section B: Nuclear, Elementary Particle and High-Energy Physics}, +pages = {1--7}, +title = {{Dark disk substructure and superfluid dark matter}}, +url = {http://arxiv.org/abs/1901.03694}, +volume = {797}, +year = {2019} +} +@article{Bolton2012, +abstract = {We present an analysis of the evolution of the central mass-density profile of massive elliptical galaxies from the SLACS and BELLS strong gravitational lens samples over the redshift interval z ≈ 0.1-0.6, based on the combination of strong-lensing aperture mass and stellar velocity-dispersion constraints. We find a significant trend toward steeper mass profiles (parameterized by the power-law density model with $\rho$ ∝ r-$\gamma$) at later cosmic times, with magnitude d 〈$\gamma$〉/dz = -0.60 ± 0.15. We show that the combined lens-galaxy sample is consistent with a non-evolving distribution of stellar velocity dispersions. Considering possible additional dependence of 〈$\gamma$〉 on lens-galaxy stellar mass, effective radius, and S{\'{e}}rsic index, we find marginal evidence for shallower mass profiles at higher masses and larger sizes, but with a significance that is subdominant to the redshift dependence. Using the results of published Monte Carlo simulations of spectroscopic lens surveys, we verify that our mass-profile evolution result cannot be explained by lensing selection biases as a function of redshift. Interpreted as a true evolutionary signal, our result suggests that major dry mergers involving off-axis trajectories play a significant role in the evolution of the average mass-density structure of massive early-type galaxies over the past 6Gyr. We also consider an alternative non-evolutionary hypothesis based on variations in the strong-lensing measurement aperture with redshift, which would imply the detection of an "inflection zone" marking the transition between the baryon-dominated and dark-matter halo-dominated regions of the lens galaxies. Further observations of the combined SLACS+BELLS sample can constrain this picture more precisely, and enable a more detailed investigation of the multivariate dependences of galaxy mass structure across cosmic time. {\textcopyright} 2012. The American Astronomical Society. All rights reserved.}, +archivePrefix = {arXiv}, +arxivId = {1201.2988}, +author = {Bolton, Adam S. and Brownstein, Joel R. and Kochanek, Christopher S. and Shu, Yiping and Schlegel, David J. and Eisenstein, Daniel J. and Wake, David A. and Connolly, Natalia and Maraston, Claudia and Arneson, Ryan A. and Weaver, Benjamin A.}, +doi = {10.1088/0004-637X/757/1/82}, +eprint = {1201.2988}, +issn = {15384357}, +journal = {Astrophysical Journal}, +keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,galaxies: structure,Gravitational lensing: strong}, +number = {1}, +pages = {82}, +title = {{The BOSS emission-line lens survey. II. Investigating mass-density profile evolution in the SLACS+BELLS strong gravitational lens sample}}, +url = {http://arxiv.org/abs/1201.2988}, +volume = {757}, +year = {2012} +} +@article{Collett2015, +abstract = {Ongoing and future imaging surveys represent significant improvements in depth, area, and seeing compared to current data sets. These improvements offer the opportunity to discover up to three orders of magnitude more galaxy-galaxy strong lenses than are currently known. In this work we forecast the number of lenses that will be discoverable in forthcoming surveys and simulate their properties. We generate a population of statistically realistic strong lenses and simulate observations of this population for the Dark Energy Survey (DES), the Large Synoptic Survey Telescope (LSST), and Euclid surveys. We verify our model against the galaxy-scale lens search of the Canada-France-Hawaii Telescope Legacy Survey, predicting 250 discoverable lenses compared to 220 found by Gavazzi et al. The predicted Einstein radius distribution is also remarkably similar to that found by Sonnenfeld et al. For future surveys we find that, assuming Poisson limited lens galaxy subtraction, searches of the DES, LSST, and Euclid data sets should discover 2400, 120000, and 170000 galaxy-galaxy strong lenses, respectively. Finders using blue-minus-red (g - i) difference imaging for lens subtraction can discover 1300 and 62000 lenses in DES and LSST. The uncertainties on the model are dominated by the high-redshift source population, which typically gives fractional errors on the discoverable lens number at the level of tens of percent. We find that doubling the signal-to-noise ratio required for a lens to be detectable approximately halves the number of detectable lenses in each survey, indicating the importance of understanding the selection function and the sensitivity of future lens finders in interpreting strong lens statistics. We make our population forecasting and simulated observation codes publicly available so that the selection function of strong lens finders can easily be calibrated.}, +archivePrefix = {arXiv}, +arxivId = {1507.02657}, +author = {Collett, Thomas E.}, +doi = {10.1088/0004-637X/811/1/20}, +eprint = {1507.02657}, +isbn = {0004-637x}, +issn = {15384357}, +journal = {Astrophysical Journal}, +keywords = {gravitational lensing: strong}, +number = {1}, +pages = {20}, +title = {{The population of galaxy-galaxy strong lenses in forthcoming optical imaging surveys}}, +url = {http://arxiv.org/abs/1507.02657}, +volume = {811}, +year = {2015} +} +@article{Czoske2012, +abstract = {This paper presents the full Very Large Telescope (VLT)/VIMOS-IFU data set and related data products from an ESO Large Programme with the observational goal of obtaining two-dimensional kinematic data of early-type lens galaxies, out to one effective radius. The sample consists of 17 early-type galaxies (ETGs) selected from the SLACS gravitational-lens survey. The galaxies cover the redshift range from 0.08 to 0.35 and have stellar velocity dispersions between 200 and 350 kms-1. This programme is complemented by a similar observational programme on Keck, using long-slit spectroscopy. In combination with multi-band imaging data, the kinematic data provide stringent constraints on the inner mass profiles of ETGs beyond the local Universe. Our Large Programme thus extends studies of nearby ETGs (e.g. SAURON/ATLAS3D) by an order of magnitude in distance and towards higher masses. We provide an overview of our observational strategy, the data products (luminosity-weighted spectra andHubble Space Telescopeimages) and derived products (i.e. two-dimensional fields of velocity dispersions and streaming motions) that have been used in a number of published and forthcoming lensing, kinematic and stellar-population studies. These studies also pave the way for future studies of ETGs atz≈ 1 with the upcoming extremely large telescopes. {\textcopyright} 2011 The Authors MNRAS {\textcopyright} 2011 RAS.}, +archivePrefix = {arXiv}, +arxivId = {1108.0577}, +author = {Czoske, Oliver and Barnab{\`{e}}, Matteo and Koopmans, L{\'{e}}on V.E. and Treu, Tommaso and Bolton, Adam S.}, +doi = {10.1111/j.1365-2966.2011.19726.x}, +eprint = {1108.0577}, +isbn = {00358711}, +issn = {00358711}, +journal = {MNRAS}, +keywords = {galaxies: elliptical and lenticular, cD,galaxies: kinematics and dynamics,galaxies: structure,Gravitational lensing: strong,Techniques: spectroscopic}, +number = {1}, +pages = {656--668}, +title = {{Two-dimensional kinematics of SLACS lenses - IV. The complete VLT-VIMOS data set}}, +volume = {419}, +year = {2012} +} +@article{Dye2014, +abstract = {We have determined the mass density radial profiles of the first five strong gravitational lens systems discovered by the Herschel Astrophysical Terahertz Large Area Survey. We present an enhancement of the semilinear lens inversion method of Warren {\&} Dye which allows simultaneous reconstruction of several different wavebands and apply this to dual-band imaging of the lenses acquired with the Hubble Space Telescope. The five systems analysed here have lens redshifts which span a range 0.22 ≤ z ≤ 0.94. Our findings are consistent with other studies by concluding that: (1) the logarithmic slope of the total mass density profile steepens with decreasing redshift; (2) the slope is positively correlated with the average total projected mass density of the lens contained within half the effective radius and negatively correlated with the effective radius; (3) the fraction of dark matter contained within half the effective radius increases with increasing effective radius and increases with redshift. {\textcopyright} 2014 The Authors Published by Oxford University Press on behalf of the Royal Astronomical Society.}, +archivePrefix = {arXiv}, +arxivId = {1311.5893}, +author = {Dye, S. and Negrello, M. and Hopwood, R. and Nightingale, J. W. and Bussmann, R. S. and Amber, S. and Bourne, N. and Cooray, A. and Dariush, A. and Dunne, L. and Eales, S. A. and Gonzalez-Nuevo, J. and Ibar, E. and Ivison, R. J. and Maddox, S. and Valiante, E. and Smith, M.}, +doi = {10.1093/mnras/stu305}, +eprint = {1311.5893}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {galaxies: evolution,galaxies: structure}, +number = {3}, +pages = {2013--2025}, +title = {{Herschel*-ATLAS: Modelling the first strong gravitational lenses}}, +volume = {440}, +year = {2014} +} +@article{Enia2018, +abstract = {We perform lens modelling and source reconstruction of Sub-millimetre Array2D (SMA) data for a sample of 12 strongly lensed galaxies selected at 500$\mu$m in the Herschel Astrophysical Terahertz Large Area Survey (H-ATLAS). A previous analysis of the same data set used a single S{\'{e}}rsic profile to model the light distribution of each background galaxy. Here we model the source brightness distribution with an adaptive pixel scale scheme, extended to work in the Fourier visibility space of interferometry. We also present new SMA observations for seven other candidate lensed galaxies from theH-ATLAS sample. Our derived lens model parameters are in general consistent with previous findings. However, our estimated magnification factors, ranging from 3 to 10, are lower. The discrepancies are observed in particular where the reconstructed source hints at the presence of multiple knots of emission.We define an effective radius of the reconstructed sources based on the area in the source plane where emission is detected above 5s. We also fit the reconstructed source surface brightness with an elliptical Gaussian model. We derive a median value reff {\~{}} 1.77 kpc and a median Gaussian full width at half-maximum {\~{}}1.47 kpc. After correction for magnification, our sources have intrinsic star formation rates (SFR) {\~{}} 900-3500M⊙ yr-1, resulting in a median SFR surface density $\Sigma$SFR {\~{}} 132M⊙ yr-1 kpc-2 (or {\~{}}218M⊙ yr-1 kpc-2 for the Gaussian fit). This is consistent with that observed for other star-forming galaxies at similar redshifts, and is significantly below the Eddington limit for a radiation pressure regulated starburst.}, +archivePrefix = {arXiv}, +arxivId = {1801.01831}, +author = {Enia, A. and Negrello, M. and Gurwell, M. and Dye, S. and Rodighiero, G. and Massardi, M. and {De Zotti}, G. and Franceschini, A. and Cooray, A. and van der Werf, P. and Birkinshaw, M. and Michalowski, M. J. and Oteo, I.}, +doi = {10.1093/mnras/sty021}, +eprint = {1801.01831}, +file = {:home/jammy/.local/share/data/Mendeley Ltd./Mendeley Desktop/Downloaded/Enia et al. - 2018 - The Herschel-ATLAS Magnifications and physical sizes of 500-$\mu$m-selected strongly lensed galaxies.pdf:pdf}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {galaxies: structure,Gravitational lensing: strong,Instrumentation: interferometers}, +number = {3}, +pages = {3467--3484}, +title = {{The Herschel-ATLAS: Magnifications and physical sizes of 500-$\mu$m-selected strongly lensed galaxies}}, +url = {http://arxiv.org/abs/1801.01831}, +volume = {475}, +year = {2018} +} +@article{Hermans2019, +abstract = {Posterior inference with an intractable likelihood is becoming an increasingly common task in scientific domains which rely on sophisticated computer simulations. Typically, these mechanistic models do not admit tractable densities forcing practitioners to rely on approximations during inference. This work proposes a novel approach to address the intractability of the likelihood and the marginal model. We achieve this by learning a flexible estimator which approximates the likelihood-to-evidence ratio. The resulting amortized ratio estimator is embedded in MCMC samplers such as Metropolis-Hastings and Hamiltonian Monte Carlo to approximate the likelihood-ratio between consecutive states in the Markov chain, allowing us to draw samples from the intractable posterior. Techniques are presented to improve the numerical stability. We demonstrate our approach on a variety of benchmarks and compare against well-established approximate inference techniques. Scientific applications in high energy and astrophysics with high-dimensional observations show its applicability.}, +archivePrefix = {arXiv}, +arxivId = {1903.04057}, +author = {Hermans, Joeri and Begy, Volodimir and Louppe, Gilles}, +eprint = {1903.04057}, +file = {:home/jammy/Documents/Papers/PyAutoLens/Hermanns2019LikelihoodFree.pdf:pdf}, +number = {i}, +title = {{Likelihood-free MCMC with Amortized Approximate Likelihood Ratios}}, +url = {http://arxiv.org/abs/1903.04057}, +year = {2019} +} +@article{Koopmans2009, +abstract = {Based on 58 SLACS strong-lens early-type galaxies (ETGs) with direct total-mass and stellar-velocity dispersion measurements, we find that inside one effective radius massive elliptical galaxies with M eff ≳ 3 × 1010 M⊙ are well approximated by a power-law ellipsoid, with an average logarithmic density slope of 〈$\gamma$′ LD〉 ≡ -dlog($\rho$tot)/dlog(r) = 2.085 +0.025-0.018 (random error on mean) for isotropic orbits with $\beta$r = 0, 0.1 (syst.) and intrinsic scatter (all errors indicate the 68{\%} CL). We find no correlation of $\gamma$′LD with galaxy mass (M eff), rescaled radius (i.e., R einst/R eff) or redshift, despite intrinsic differences in density-slope between galaxies. Based on scaling relations, the average logarithmic density slope can be derived in an alternative manner, fully independent from dynamics, yielding 〈$\gamma$′SR〉 = 1.959 0.077. Agreement between the two values is reached for 〈$\beta$r〉 = 0.45 0.25, consistent with mild radial anisotropy. This agreement supports the robustness of our results, despite the increase in mass-to-light ratio with total galaxy mass: M eff L 1.3630.056V,eff. We conclude that massive ETGs are structurally close to homologous with close to isothermal total density profiles (≲10{\%} intrinsic scatter) and have at most some mild radial anisotropy. Our results provide new observational limits on galaxy formation and evolution scenarios, covering 4 Gyr look-back time. {\textcopyright} 2009. The American Astronomical Society.}, +archivePrefix = {arXiv}, +arxivId = {0906.1349}, +author = {Koopmans, L. V.E. and Bolton, A. and Treu, T. and Czoske, O. and Auger, M. W. and Barnab{\`{e}}, M. and Vegetti, S. and Gavazzi, R. and Moustakas, L. A. and Burles, S.}, +doi = {10.1088/0004-637X/703/1/L51}, +eprint = {0906.1349}, +isbn = {1522-1601 (Electronic)$\backslash$r0161-7567 (Linking)}, +issn = {15384357}, +journal = {Astrophysical Journal}, +keywords = {galaxies: structure,Gravitational lensing}, +number = {1 PART 2}, +pages = {L51--L54}, +pmid = {25414243}, +title = {{The structure and dynamics of massive early-type galaxies: On homology, isothermality, and isotropy inside one effective radius}}, +url = {http://stacks.iop.org/1538-4357/703/i=1/a=L51?key=crossref.0217bdab14c5f868ab70f18136b0acdd}, +volume = {703}, +year = {2009} +} +@article{McCully2014, +abstract = {In strong gravitational lens systems, the light bending is usually dominated by one main galaxy, but may be affected by other mass along the line of sight (LOS). Shear and convergence can be used to approximate the contributions from less significant perturbers (e.g. those that are projected far from the lens or have a small mass), but higher order effects need to be included for objects that are closer or more massive. We develop a framework for multiplane lensing that can handle an arbitrary combination of tidal planes treated with shear and convergence and planes treated exactly (i.e. including higher order terms). This framework addresses all of the traditional lensing observables including image positions, fluxes, and time delays to facilitate lens modelling that includes the non-linear effects due to mass along the LOS. It balances accuracy (accounting for higher order terms when necessary) with efficiency (compressing all other LOS effects into a set of matrices that can be calculated up front and cached for lens modelling). We identify a generalized multiplane mass sheet degeneracy, in which the effective shear and convergence are sums over the lensing planes with specific, redshift-dependent weighting factors. {\textcopyright} 2014 The Authors Published by Oxford University Press on behalf of the Royal Astronomical Society.}, +archivePrefix = {arXiv}, +arxivId = {1401.0197}, +author = {McCully, Curtis and Keeton, Charles R. and Wong, Kenneth C. and Zabludoff, Ann I.}, +doi = {10.1093/mnras/stu1316}, +eprint = {1401.0197}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {Gravitational lensing: strong,Gravitational lensing: weak}, +month = {oct}, +number = {4}, +pages = {3631--3642}, +title = {{A new hybrid framework to efficiently model lines of sight to gravitational lenses}}, +volume = {443}, +year = {2014} +} +@article{Negrello2014, +abstract = {We report on deep near-infrared observations obtained with the Wide Field Camera-3 (WFC3) onboard the Hubble Space Telescope (HST) of the first five confirmed gravitational lensing events discovered by the Herschel Astrophysical Terahertz Large Area Survey (H-ATLAS). We succeed in disentangling the background galaxy from the lens to gain separate photometry of the two components. The HST data allow us to significantly improve on previous constraints of the mass in stars of the lensed galaxy and to perform accurate lens modelling of these systems, as described in the accompanying paper by Dye et al. We fit the spectral energy distributions of the background sources from near-IR to millimetre wavelengths and use the magnification factors estimated by Dye et al. to derive the intrinsic properties of the lensed galaxies. We find these galaxies to have star-formations rates (SFR) {\~{}} 400-2000 M⊙ yr-1, with {\~{}}(6-25) × 1010 M⊙ of their baryonic mass already turned into stars. At these rates of star formation, all remaining molecular gas will be exhausted in less than {\~{}}100 Myr, reaching a final mass in stars of a few 1011 M⊙. These galaxies are thus proto-ellipticals caught during their major episode of star formation, and observed at the peak epoch (z {\~{}} 1.5-3) of the cosmic star formation history of the Universe.}, +archivePrefix = {arXiv}, +arxivId = {1311.5898}, +author = {Negrello, M. and Hopwood, R. and Dye, S. and da Cunha, E. and Serjeant, S. and Fritz, J. and Rowlands, K. and Fleuren, S. and Bussmann, R. S. and Cooray, A. and Dannerbauer, H. and Gonzalez-Nuevo, J. and Lapi, A. and Omont, A. and Amber, S. and Auld, R. and Baes, M. and Buttiglione, S. and Cava, A. and Danese, L. and Dariush, A. and {De Zotti}, G. and Dunne, L. and Eales, S. and Ibar, E. and Ivison, R. J. and Kim, S. and Leeuw, L. and Maddox, S. and Michalowski, M. J. and Massardi, M. and Pascale, E. and Pohlen, M. and Rigby, E. and Smith, D. J.B. and Sutherland, W. and Temi, P. and Wardlow, J.}, +doi = {10.1093/mnras/stu413}, +eprint = {1311.5898}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,galaxies: formation,Gravitational lensing: strong,Infrared: galaxies,Submillimetre: galaxies}, +number = {3}, +pages = {1999--2012}, +title = {{Herschel *-ATLAS: Deep HST/WFC3 imaging of strongly lensed submillimetre galaxies}}, +url = {http://arxiv.org/abs/1311.5898}, +volume = {440}, +year = {2014} +} +@article{Nightingale2015, +abstract = {We present a new pixelized method for the inversion of gravitationally lensed extended source images which we term adaptive semi-linear inversion (SLI). At the heart of the method is an h-means clustering algorithm which is used to derive a source plane pixelization that adapts to the lens model magnification. The distinguishing feature of adaptive SLI is that every pixelization is derived from a random initialization, ensuring that data discretization is performed in a completely different and unique way for every model parameter set. We compare standard SLI on a fixed source pixel grid with the new method and demonstrate the shortcomings of the former when modelling singular power-law ellipsoid (SPLE) lens profiles. In particular, we demonstrate the superior reliability and efficiency of adaptive SLI which, by design, fixes the number of degrees of freedom (NDOF) of the optimization and thereby removes biases present with other methods that allow the NDOF to vary. In addition, we highlight the importance of data discretization in pixel-based inversion methods, showing that adaptive SLI averages over significant systematics that are present when a fixed source pixel grid is used. In the case of the SPLE lens profile, we show how the method successfully samples its highly degenerate posterior probability distribution function with a single nonlinear search. The robustness of adaptive SLI provides a firm foundation for the development of a strong lens modelling pipeline, which will become necessary in the short-term future to cope with the increasing rate of discovery of new strong lens systems.}, +archivePrefix = {arXiv}, +arxivId = {1412.7436}, +author = {Nightingale, J. W. and Dye, S.}, +doi = {10.1093/mnras/stv1455}, +eprint = {1412.7436}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {galaxies: evolution,galaxies: structure,Methods: observational}, +month = {sep}, +number = {3}, +pages = {2940--2959}, +title = {{Adaptive semi-linear inversion of strong gravitational lens imaging}}, +volume = {452}, +year = {2015} +} +@article{Nightingale2018, +abstract = {This work presents AutoLens, the first entirely automated modeling suite for the analysis of galaxy-scale strong gravitational lenses. AutoLens simultaneously models the lens galaxy's light and mass whilst reconstructing the extended source galaxy on an adaptive pixel-grid. The method's approach to source-plane discretization is amorphous, adapting its clustering and regularization to the intrinsic properties of the lensed source. The lens's light is fitted using a superposition of Sersic functions, allowing AutoLens to cleanly deblend its light from the source. Single-component mass models representing the lens's total mass density profile are demonstrated, which in conjunction with light modeling can detect central images using a centrally cored profile. Decomposed mass modeling is also shown, which can fully decouple a lens's light and dark matter and determine whether the two components are geometrically aligned. The complexity of the light and mass models is automatically chosen via Bayesian model comparison. These steps form AutoLens's automated analysis pipeline, such that all results in this work are generated without any user intervention. This is rigorously tested on a large suite of simulated images, assessing its performance on a broad range of lens profiles, source morphologies, and lensing geometries. The method's performance is excellent, with accurate light, mass, and source profiles inferred for data sets representative of both existing Hubble imaging and future Euclid wide-field observations.}, +archivePrefix = {arXiv}, +arxivId = {1708.07377}, +author = {Nightingale, J. W. and Dye, S. and Massey, Richard J.}, +doi = {10.1093/mnras/sty1264}, +eprint = {1708.07377}, +file = {:home/jammy/Documents/Papers{\_}Me/AutoLensChangesMarked.pdf:pdf}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {Galaxy: structure,Gravitational lensing,Methods: data analysis}, +number = {4}, +pages = {4738--4784}, +title = {{AutoLens: Automated modeling of a strong lens's light, mass, and source}}, +url = {https://academic.oup.com/mnras/article/478/4/4738/5001434}, +volume = {478}, +year = {2018} +} +@article{Nightingale2019, +abstract = {We investigate how strong gravitational lensing can test contemporary models of massive elliptical (ME) galaxy formation, by combining a traditional decomposition of their visible stellar distribution with a lensing analysis of their mass distribution. As a proof of concept, we study a sample of three ME lenses, observing that all are composed of two distinct baryonic structures, a 'red' central bulge surrounded by an extended envelope of stellar material. Whilst these two components look photometrically similar, their distinct lensing effects permit a clean decomposition of their mass structure. This allows us to infer two key pieces of information about each lens galaxy: (i) the stellar mass distribution (without invoking stellar populations models) and (ii) the inner dark matter halo mass. We argue that these two measurements are crucial to testing models of ME formation, as the stellar mass profile provides a diagnostic of baryonic accretion and feedback whilst the dark matter mass places each galaxy in the context of LCDM large-scale structure formation. We also detect large rotational offsets between the two stellar components and a lopsidedness in their outer mass distributions, which hold further information on the evolution of each ME. Finally, we discuss how this approach can be extended to galaxies of all Hubble types and what implication our results have for studies of strong gravitational lensing.}, +archivePrefix = {arXiv}, +arxivId = {1901.07801}, +author = {Nightingale, J. W. and Massey, Richard J. and Harvey, David R. and Cooper, Andrew P. and Etherington, Amy and Tam, Sut Ieng and Hayes, Richard G.}, +doi = {10.1093/mnras/stz2220}, +eprint = {1901.07801}, +file = {:home/jammy/Documents/Papers{\_}Me/Gal{\_}Structure{\_}Final/GalaxyStructure.pdf:pdf}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {galaxies: Evolution,galaxies: Formation,Gravitational lensing: Strong}, +number = {2}, +pages = {2049--2068}, +title = {{Galaxy structure with strong gravitational lensing: Decomposing the internal mass distribution of massive elliptical galaxies}}, +url = {http://arxiv.org/abs/1901.07801}, +volume = {489}, +year = {2019} +} +@article{Sonnenfeld2015, +abstract = {We investigate the cosmic evolution of the internal structure of massive early-type galaxies over half of the age of the universe. We perform a joint lensing and stellar dynamics analysis of a sample of 81 strong lenses from the Strong Lensing Legacy Survey and Sloan ACS Lens Survey and combine the results with a hierarchical Bayesian inference method to measure the distribution of dark matter mass and stellar initial mass function (IMF) across the population of massive early-type galaxies. Lensing selection effects are taken into account. We find that the dark matter mass projected within the inner 5 kpc increases for increasing redshift, decreases for increasing stellar mass density, but is roughly constant along the evolutionary tracks of early-type galaxies. The average dark matter slope is consistent with that of a Navarro-Frenk-White profile, but is not well constrained. The stellar IMF normalization is close to a Salpeter IMF at log M ∗ = 11.5 and scales strongly with increasing stellar mass. No dependence of the IMF on redshift or stellar mass density is detected. The anti-correlation between dark matter mass and stellar mass density supports the idea of mergers being more frequent in more massive dark matter halos.}, +archivePrefix = {arXiv}, +arxivId = {1410.1881}, +author = {Sonnenfeld, Alessandro and Treu, Tommaso and Marshall, Philip J. and Suyu, Sherry H. and Gavazzi, Rapha{\"{e}}l and Auger, Matthew W. and Nipoti, Carlo}, +doi = {10.1088/0004-637X/800/2/94}, +eprint = {1410.1881}, +issn = {15384357}, +journal = {Astrophysical Journal}, +keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,gravitational lensing: strong}, +number = {2}, +pages = {94}, +title = {{The sl2s galaxy-scale lens sample. V. Dark matter halos and stellar imf of massive early-type galaxies out to redshift 0.8}}, +url = {http://arxiv.org/abs/1410.1881}, +volume = {800}, +year = {2015} +} +@article{Suyu2016, +abstract = {Strong gravitational lens systems with time delays between the multiple images allow measurements of time-delay distances, which are primarily sensitive to the Hubble constant that is key to probing dark energy, neutrino physics and the spatial curvature of the Universe, as well as discovering new physics. We present H0LiCOW (H0 Lenses in COSMOGRAIL's Wellspring), a program that aims to measure H0 with {\textless} 3.5 per cent uncertainty from five lens systems (B1608+656, RXJ1131-1231, HE 0435-1223, WFI2033-4723 and HE 1104-1805). We have been acquiring (1) time delays through COSMOGRAIL and Very Large Array2D monitoring, (2) high-resolution Hubble Space Telescope imaging for the lens mass modelling, (3) wide-field imaging and spectroscopy to characterize the lens environment and (4) moderate-resolution spectroscopy to obtain the stellar velocity dispersion of the lenses for mass modelling. In cosmological models with one-parameter extension to flat $\Lambda$ cold dark matter, we expect to measure H0 to {\textless} 3.5 per cent in most models, spatial curvature $\Omega$k to 0.004, w to 0.14 and the effective number of neutrino species to 0.2 (1$\sigma$ uncertainties) when combined with current cosmic microwave background (CMB) experiments. These are, respectively, a factor of {\~{}}15, {\~{}}2 and {\~{}}1.5 tighter than CMB alone. Our data set will further enable us to study the stellar initial mass function of the lens galaxies, and the co-evolution of supermassive black holes and their host galaxies. This program will provide a foundation for extracting cosmological distances from the hundreds of time-delay lenses that are expected to be discovered in current and future surveys.}, +archivePrefix = {arXiv}, +arxivId = {1607.00017}, +author = {Suyu, S. H. and Bonvin, V. and Courbin, F. and Fassnacht, C. D. and Rusu, C. E. and Sluse, D. and Treu, T. and Wong, K. C. and Auger, M. W. and Ding, X. and Hilbert, S. and Marshall, P. J. and Rumbaugh, N. and Sonnenfeld, A. and Tewes, M. and Tihhonova, O. and Agnello, A. and Blandford, R. D. and Chen, G. C.F. and Collett, T. and Koopmans, L. V.E. and Liao, K. and Meylan, G. and Spiniello, C.}, +doi = {10.1093/mnras/stx483}, +eprint = {1607.00017}, +file = {:home/jammy/.local/share/data/Mendeley Ltd./Mendeley Desktop/Downloaded/Suyu et al. - 2017 - H0LiCOW - I. H0 Lenses in COSMOGRAIL's Wellspring Program overview.pdf:pdf}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {Cosmological parameters,Distance scale,galaxies: structure,Gravitational lensing: strong,HE 0435-1223,HE 1104-1805,Quasars: individual:B1608+656,RXJ1131-1231,WFI2033-4723}, +number = {3}, +pages = {2590--2604}, +title = {{H0LiCOW - I. H0 Lenses in COSMOGRAIL's Wellspring: Program overview}}, +url = {http://arxiv.org/abs/1607.00017{\%}0Ahttp://dx.doi.org/10.1093/mnras/stx483}, +volume = {468}, +year = {2017} +} +@article{Treu2009, +abstract = {We study the relation between the internal structure of early-type galaxies and their environment using 70 strong gravitational lenses from the SLACS Survey. The Sloan Digital Sky Survey (SDSS) database is used to determine two measures of overdensity of galaxies around each lens - the projected number density of galaxies inside the tenth nearest neighbor ($\Sigma$10) and within a cone of radius one h -1 Mpc (D 1). Our main results are as follows. (1) The average overdensity is somewhat larger than unity, consistent with lenses preferring overdense environments as expected for massive early-type galaxies (12/70 lenses are in known groups/clusters). (2) The distribution of overdensities is indistinguishable from that of "twin" nonlens galaxies selected from SDSS to have the same redshift and stellar velocity dispersion $\sigma$*. Thus, within our errors, lens galaxies are an unbiased population, and the SLACS results can be generalized to the overall population of early-type galaxies. (3) Typical contributions from external mass distribution are no more than a few percent in local mass density, reaching 10-20{\%} (0.05-0.10 external convergence) only in the most extreme overdensities. (4) No significant correlation between overdensity and slope of the mass-density profile of the lens galaxies is found. (5) Satellite galaxies (those with a more luminous companion) have marginally steeper mass-density profiles (as quantified by f SIE = $\sigma$*/$\sigma$SIE = 1.12 ± 0.05 versus 1.01 ± 0.01) and smaller dynamically normalized mass enclosed within the Einstein radius ($\Delta$log M Ein/M dim differs by -0.09 ± 0.03 dex) than central galaxies (those without). This result suggests that tidal stripping may affect the mass structure of early-type galaxies down to kpc scales probed by strong lensing, when they fall into larger structures. {\textcopyright} 2009. The American Astronomical Society. All rights reserved.}, +archivePrefix = {arXiv}, +arxivId = {0806.1056}, +author = {Treu, Tommaso and Gavazzi, Rapha{\"{e}}l and Gorecki, Alexia and Marshall, Philip J. and Koopmans, L{\'{e}}on V.E. and Bolton, Adam S. and Moustakas, Leonidas A. and Burles, Scott}, +doi = {10.1088/0004-637X/690/1/670}, +eprint = {0806.1056}, +issn = {15384357}, +journal = {Astrophysical Journal}, +keywords = {galaxies: elliptical and lenticular, cD,galaxies: evolution,galaxies: formation,galaxies: structure,Gravitational lensing}, +number = {1}, +pages = {670--682}, +title = {{The slacs survey. VIII. the relation between environment and internal structure of early-type galaxies}}, +url = {http://stacks.iop.org/0004-637X/690/i=1/a=670?key=crossref.f6cfae6390f195584737e04966a8c778}, +volume = {690}, +year = {2009} +} +@article{Vegetti2009, +abstract = {We introduce a new adaptive and fully Bayesian grid-based method to model strong gravitational lenses with extended images. The primary goal of this method is to quantify the level of luminous and dark mass substructure in massive galaxies, through their effect on highly magnified arcs and Einstein rings. The method is adaptive on the source plane, where a Delaunay tessellation is defined according to the lens mapping of a regular grid on to the source plane. The Bayesian penalty function allows us to recover the best non-linear potential-model parameters and/or a grid-based potential correction and to objectively quantify the level of regularization for both the source and potential. In addition, we implement a Nested-Sampling technique to quantify the errors on all non-linear mass model parameters - marginalized over all source and regularization parameters - and allow an objective ranking of different potential models in terms of the marginalized evidence. In particular, we are interested in comparing very smooth lens mass models with ones that contain mass substructures. The algorithm has been tested on a range of simulated data sets, created from a model of a realistic lens system. One of the lens systems is characterized by a smooth potential with a power-law density profile, 12 include a Navarro, Frenk and White (NFW) dark matter substructure of different masses and at different positions and one contains two NFW dark substructures with the same mass but with different positions. Reconstruction of the source and lens potential for all of these systems shows the method is able, in a realistic scenario, to identify perturbations with masses ≳107 M ⊙ when located on the Einstein ring. For positions both inside and outside of the ring, masses of at least 109 M⊙ are required (i.e. roughly the Einstein ring of the perturber needs to overlap with that of the main lens). Our method provides a fully novel and objective test of mass substructure in massive galaxies. {\textcopyright} 2008 RAS.}, +archivePrefix = {arXiv}, +arxivId = {0805.0201}, +author = {Vegetti, S. and Koopmans, L. V.E.}, +doi = {10.1111/j.1365-2966.2008.14005.x}, +eprint = {0805.0201}, +isbn = {0892-0915 (Print)$\backslash$r0892-0915 (Linking)}, +issn = {00358711}, +journal = {MNRAS}, +keywords = {Dark matter,galaxies: haloes,galaxies: structure,Gravitational lensing}, +month = {jan}, +number = {3}, +pages = {945--963}, +pmid = {20829068}, +title = {{Bayesian strong gravitational-lens modelling on adaptive grids: Objective detection of mass substructure in galaxies}}, +volume = {392}, +year = {2009} +} +@article{Vegetti2014, +abstract = {We present the results of a search for galaxy substructures in a sample of 11 gravitational lens galaxies from the Sloan Lens ACS Survey by Bolton et al. We find no significant detection of mass clumps, except for a luminous satellite in the system SDSS J0956+5110. We use these non-detections, in combination with a previous detection in the system SDSS J0946+1006, to derive constraints on the substructure mass function in massive early-type host galaxies with an average redshift zlens {\~{}}0.2 and an average velocity dispersion seff {\~{}}270 km s-1. We perform a Bayesian inference on the substructure mass function, within a median region of about 32 kpc2 around the Einstein radius (Rein {\~{}}4.2 kpc). We infer a mean projected substructure mass fraction f = 0.0076+0.0208-0.0052 at the 68 per cent confidence level and a substructure mass function slopea {\textless} 2.93 at the 95 per cent confidence level for a uniform prior probability density on a. For a Gaussian prior based on cold dark matter (CDM) simulations, we infer f = 0.0064+0.0080-0.0042 and a slope of a =1.90+0.098-0.098 at the 68 per cent confidence level. Since only one substructure was detected in the full sample, we have little information on the mass function slope, which is therefore poorly constrained (i.e. the Bayes factor shows no positive preference for any of the two models). The inferred fraction is consistent with the expectations from CDM simulations and with inference from flux ratio anomalies at the 68 per cent confidence level. {\textcopyright} 2014 The Authors Published by Oxford University Press on behalf of the Royal Astronomical Society.}, +archivePrefix = {arXiv}, +arxivId = {1405.3666}, +author = {Vegetti, S. and Koopmans, L. V.E. and Auger, M. W. and Treu, T. and Bolton, A. S.}, +doi = {10.1093/mnras/stu943}, +eprint = {1405.3666}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {galaxies,Structure}, +month = {aug}, +number = {3}, +pages = {2017--2035}, +title = {{Inference of the cold dark matter substructure mass function at z = 0.2 using strong gravitational lenses}}, +volume = {442}, +year = {2014} +} +@article{Atek2015, +abstract = {Exploiting the power of gravitational lensing, the Hubble Frontier Fields (HFF) program aims at observing six massive galaxy clusters to explore the distant universe far beyond the limits of blank field surveys. Using the complete Hubble Space Telescope observations of the first HFF cluster A2744, we report the detection of 50 galaxy candidates at z ∼ 7 and eight candidates at z ∼ 8 in a total survey area of 0.96 arcmin2 in the source plane. Three of these galaxies are multiply imaged by the lensing cluster. Using an updated model of the mass distribution in the cluster we were able to calculate the magnification factor and the effective survey volume for each galaxy in order to compute the ultraviolet galaxy luminosity function (LF) at both redshifts 7 and 8. Our new measurements reliably extend the z ∼ 7 UV LF down to an absolute magnitude of MUV ∼ -15.5. We find a characteristic magnitude of M∗UV = -20.90-0.73+0.90 mag and a faint-end slope $\alpha$ = -2.01-0.28+0.20,close to previous determinations in blank fields. We show here for the first time that this slope remains steep down to very faint luminosities of 0.01 L∗. Although prone to large uncertainties, our results at z ∼ 8 also seem to confirm a steep faint-end slope below 0.1 L∗. The HFF program is therefore providing an extremely efficient way to study the faintest galaxy populations at z {\textgreater} 7 that would otherwise be inaccessible with current instrumentation. The full sample of six galaxy clusters will provide even better constraints on the buildup of galaxies at early epochs and their contribution to cosmic reionization.}, +archivePrefix = {arXiv}, +arxivId = {1409.0512}, +author = {Atek, Hakim and Richard, Johan and Kneib, Jean Paul and Jauzac, Mathilde and Schaerer, Daniel and Clement, Benjamin and Limousin, Marceau and Jullo, Eric and Natarajan, Priyamvada and Egami, Eiichi and Ebeling, Harald}, +doi = {10.1088/0004-637X/800/1/18}, +eprint = {1409.0512}, +file = {:home/jammy/Documents/Papers/StrongLensCluster/Atek2015HFFUV.pdf:pdf}, +issn = {15384357}, +journal = {Astrophysical Journal}, +keywords = {galaxies: evolution,galaxies: high-redshift,galaxies: luminosity function, mass function,Gravitational lensing: strong}, +number = {1}, +title = {{New constraints on the faint end of the UV luminosity function at z ∼ 7-8 using the gravitational lensing of the hubble frontier fields cluster A2744}}, +volume = {800}, +year = {2015} +} +@article{Jullo2010, +abstract = {Current efforts in observational cosmology are focused on characterizing the mass-energy content of the universe. We present results from a geometric test based on strong lensing in galaxy clusters. Based on Hubble Space Telescope images and extensive ground-based spectroscopic follow-up of the massive galaxy cluster Abell 1689, we used a parametric model to simultaneously constrain the cluster mass distribution and dark energy equation of state. Combining our cosmological constraints with those from x-ray clusters and the Wilkinson Microwave Anisotropy Probe 5-year data gives $\Omega$m = 0.25 ±0.05 and Wx = -0.97 ±0.07, which are consistent with results from other methods. Inclusion of our method with all other available techniques brings down the current 2$\sigma$ contours on the dark energy equation-of-state parameter wx by ∼30{\%}.}, +archivePrefix = {arXiv}, +arxivId = {1008.4802}, +author = {Jullo, Eric and Natarajan, Priyamvada and Kneib, Jean Paul and D'Aloisio, Anson and Limousin, Marceau and Richard, Johan and Schimd, Carlo}, +doi = {10.1126/science.1185759}, +eprint = {1008.4802}, +file = {:home/jammy/Documents/Papers/StrongLensCluster/Jullo2008CosmologicalConstraints.pdf:pdf}, +issn = {00368075}, +journal = {Science}, +number = {5994}, +pages = {924--927}, +title = {{Cosmological constraints from strong gravitational lensing in clusters of galaxies}}, +volume = {329}, +year = {2010} +} +@article{Richard2014, +abstract = {Extending over three Hubble Space Telescope (HST) cycles, the Hubble Frontier Fields (HFF) initiative constitutes the largest commitment ever of HST time to the exploration of the distant Universe via gravitational lensing by massive galaxy clusters. Here, we present models of the mass distribution in the six HFF cluster lenses, derived from a joint strong- and weak-lensing analysis anchored by a total of 88 multiple-image systems identified in existing HST data. The resulting maps of the projected mass distribution and of the gravitational magnification effectively calibrate the HFF clusters as gravitational telescopes. Allowing the computation of search areas in the source plane, these maps are provided to the community to facilitate the exploitation of forthcoming HFF data for quantitative studies of the gravitationally lensed population of background galaxies. Our models of the gravitational magnification afforded by the HFF clusters allow us to quantify the lensing-induced boost in sensitivity over blank-field observations and predict that galaxies at z {\textgreater} 10 and as faint as m(AB) = 32 will be detectable, up to 2 mag fainter than the limit of the Hubble Ultra Deep Field.}, +author = {Richard, Johan and Jauzac, Mathilde and Limousin, Marceau and Jullo, Eric and Cl{\'{e}}ment, Benjamin and Ebeling, Harald and Kneib, Jean Paul and Atek, Hakim and Natarajan, Priya and Egami, Eiichi and Livermore, Rachael and Bower, Richard}, +doi = {10.1093/mnras/stu1395}, +file = {:home/jammy/Documents/Papers/StrongLensCluster/Richard2017HFFModels.pdf:pdf}, +issn = {13652966}, +journal = {MNRAS}, +keywords = {galaxies clusters: individual: A1063S,galaxies clusters: individual: Abell 370,galaxies clusters: individual: MACS J0416.1-2403,galaxies clusters: individual: MACS J0717.5+3745,galaxies clusters: individual: MACS J1149.5+2223,galaxies clusters: individual: Abell 2744}, +number = {1}, +pages = {268--289}, +title = {{Mass and magnification maps for the hubble space telescope frontier fields clusters: Implications for high-redshift studies}}, +volume = {444}, +year = {2014} +} +@article{Powell2020, + author = {Powell, Devon and Vegetti, Simona and McKean, John P and Spingola, Cristiana and Rizzo, Francesca and Stacey, Hannah R}, + title = "{A novel approach to visibility-space modelling of interferometric gravitational lens observations at high angular resolution}", + journal = {MNRAS}, + volume = {501}, + number = {1}, + pages = {515-530}, + year = {2020}, + month = {09}, + abstract = "{We present a new gravitational lens modelling technique designed to model high-resolution interferometric observations with large numbers of visibilities without the need to pre-average the data in time or frequency. We demonstrate the accuracy of the method using validation tests on mock observations. Using small data sets with ∼103 visibilities, we first compare our approach with the more traditional direct Fourier transform (DFT) implementation and direct linear solver. Our tests indicate that our source inversion is indistinguishable from that of the DFT. Our method also infers lens parameters to within 1 to 2 per cent of both the ground truth and DFT, given sufficiently high signal-to-noise ratio (SNR). When the SNR is as low as 5, both approaches lead to errors of several tens of per cent in the lens parameters and a severely disrupted source structure, indicating that this is related to the SNR and choice of priors rather than the modelling technique itself. We then analyse a large data set with ∼108 visibilities and a SNR matching real global Very Long Baseline Interferometry observations of the gravitational lens system MG J0751+2716. The size of the data is such that it cannot be modelled with traditional implementations. Using our novel technique, we find that we can infer the lens parameters and the source brightness distribution, respectively, with an RMS error of 0.25 and 0.97 per cent relative to the ground truth.}", + issn = {0035-8711}, + doi = {10.1093/mnras/staa2740}, + url = {https://doi.org/10.1093/mnras/staa2740}, + eprint = {https://academic.oup.com/mnras/article-pdf/501/1/515/35069118/staa2740.pdf}, +} +@article{Birrer2018a, +abstract = {We present lenstronomy, a multi-purpose open-source gravitational lens modelling pythonpackage. lenstronomy is able to reconstruct the lens mass and surface brightness distributions of strong lensing systems using forward modelling. lenstronomy supports a wide range of analytic lens and light models in arbitrary combination. The software is also able to reconstruct complex extended sources (Birrer et. al 2015) as well as being able to model point sources. We designed lenstronomy to be stable, flexible and numerically accurate, with a clear user interface that could be deployed across different platforms. Throughout its development, we have actively used lenstronomy to make several measurements including deriving constraints on dark matter properties in strong lenses, measuring the expansion history of the universe with time-delay cosmography, measuring cosmic shear with Einstein rings and decomposing quasar and host galaxy light. The software is distributed under the MIT license. The documentation, starter guide, example notebooks, source code and installation guidelines can be found at https://lenstronomy.readthedocs.io.}, +archivePrefix = {arXiv}, +arxivId = {1803.09746}, +author = {Birrer, Simon and Amara, Adam}, +doi = {10.1016/j.dark.2018.11.002}, +eprint = {1803.09746}, +file = {:home/jammy/Documents/Papers/Strong{\_}Lens/Biirrer2018lenstronomy.pdf:pdf}, +issn = {22126864}, +journal = {Physics of the Dark Universe}, +keywords = {Gravitational lensing,Image simulations,Software}, +pages = {189--201}, +title = {{lenstronomy: Multi-purpose gravitational lens modelling software package}}, +volume = {22}, +year = {2018} +} +@ARTICLE{spilker16a, + author = {{Spilker}, J.~S. and {Marrone}, D.~P. and {Aravena}, M. and + {B{\'e}thermin}, M. and {Bothwell}, M.~S. and {Carlstrom}, J.~E. and + {Chapman}, S.~C. and {Crawford}, T.~M. and {de Breuck}, C. and + {Fassnacht}, C.~D. and {Gonzalez}, A.~H. and {Greve}, T.~R. and + {Hezaveh}, Y. and {Litke}, K. and {Ma}, J. and {Malkan}, M. and + {Rotermund}, K.~M. and {Strandet}, M. and {Vieira}, J.~D. and + {Weiss}, A. and {Welikala}, N.}, + title = "{ALMA Imaging and Gravitational Lens Models of South Pole Telescope{\mdash}Selected Dusty, Star-Forming galaxies at High Redshifts}", + journal = {\apj}, + archivePrefix = "arXiv", + eprint = {1604.05723}, + keywords = {galaxies: high-redshift, galaxies: ISM, galaxies: star formation }, + year = 2016, + month = aug, + volume = 826, + eid = {112}, + pages = {112}, + doi = {10.3847/0004-637X/826/2/112}, + adsurl = {http://adsabs.harvard.edu/abs/2016ApJ...826..112S}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} diff --git a/paper/paper.json b/paper/paper.json index 8d631b85d..6d73c42f0 100644 --- a/paper/paper.json +++ b/paper/paper.json @@ -1,23 +1,23 @@ -{ - "@context": "https://raw.githubusercontent.com/codemeta/codemeta/main/codemeta.jsonld", - "@type": "Code", - "author": [ - { - "@id": "http://orcid.org/0000-0002-8987-7401", - "@type": "Person", - "email": "james.w.nightingale@durham.ac.uk", - "name": "James W. Nightingale", - "affiliation": "Institute for Computational Cosmology, Durham University" - } - ], - "identifier": "", - "codeRepository": "https://github.com/Jammy2211/PyAutoLens", - "datePublished": "2020-10-26", - "dateModified": "2020-10-26", - "dateCreated": "2020-10-26", - "description": "PyAutoLens`: Open-Source Strong Gravitational Lensingg", - "keywords": "Python, gravitational lensing, cosmology, galaxy formation and evolution", - "license": "MIT", - "title": "PyAutoLens", - "version": "v0.64.2" +{ + "@context": "https://raw.githubusercontent.com/codemeta/codemeta/main/codemeta.jsonld", + "@type": "Code", + "author": [ + { + "@id": "http://orcid.org/0000-0002-8987-7401", + "@type": "Person", + "email": "james.w.nightingale@durham.ac.uk", + "name": "James W. Nightingale", + "affiliation": "Institute for Computational Cosmology, Durham University" + } + ], + "identifier": "", + "codeRepository": "https://github.com/Jammy2211/PyAutoLens", + "datePublished": "2020-10-26", + "dateModified": "2020-10-26", + "dateCreated": "2020-10-26", + "description": "PyAutoLens`: Open-Source Strong Gravitational Lensingg", + "keywords": "Python, gravitational lensing, cosmology, galaxy formation and evolution", + "license": "MIT", + "title": "PyAutoLens", + "version": "v0.64.2" } \ No newline at end of file diff --git a/paper/paper.md b/paper/paper.md index 0189a1856..7bb55c3b9 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -1,227 +1,227 @@ ---- -title: "`PyAutoLens`: Open-Source Lens Modeling" -tags: - - astronomy - - galaxy formation and evolution - - galaxies - - interferometry - - Python -authors: - - name: James. W. Nightingale - orcid: 0000-0002-8987-7401 - affiliation: 1 - - name: Richard G. Hayes - affiliation: 1 - - name: Aristeidis Amvrosiadis - orcid: 0000-0002-4465-1564 - affiliation: 1 - - name: Ashley Kelly - orcid: 0000-0003-3850-4469 - affiliation: 1 - - name: Amy Etherington - affiliation: 1 - - name: Qiuhan He - orcid: 0000-0003-3672-9365 - affiliation: 1 - - name: Nan Li - orcid: 0000-0001-6800-7389 - affiliation: 2 - - name: XiaoYue Cao - affiliation: 3 - - name: Jonathan Frawley - orcid: 0000-0002-9437-7399 - affiliation: 4 - - name: Shaun Cole - orcid: 0000-0002-5954-7903 - affiliation: 1 - - name: Andrea Enia - orcid: 0000-0002-0200-2857 - affiliation: 5 - - name: Carlos S. Frenk - orcid: 0000-0002-2338-716X - affiliation: 1 - - name: David R. Harvey - orcid: 0000-0002-6066-6707 - affiliation: 6 - - name: Ran Li - orcid: 0000-0003-3899-0612 - affiliation: 3 - - name: Richard J. Massey - orcid: 0000-0002-6085-3780 - affiliation: 1 - - name: Mattia Negrello - orcid: 0000-0002-7925-7663 - affiliation: 7 - - name: Andrew Robertson - orcid: 0000-0002-0086-0524 - affiliation: 1 -affiliations: - - name: Institute for Computational Cosmology, Stockton Rd, Durham DH1 3LE - index: 1 - - name: Key Laboratory of Space Astronomy and Technology, National Astronomical Observatories, Chinese Academy of Sciences, Beijing 100101, China - index: 2 - - name: National Astronomical Observatories, Chinese Academy of Sciences, 20A Datun Road, Chaoyang District, Beijing 100012, China - index: 3 - - name: Advanced Research Computing, Durham University, Durham DH1 3LE - index: 4 - - name: Dipartimento di Fisica e Astronomia, Università degli Studi di Bologna, Via Berti Pichat 6/2, I-40127 Bologna, Italy - index: 5 - - name: Lorentz Institute, Leiden University, Niels Bohrweg 2, Leiden, NL-2333 CA, The Netherlands - index: 6 - - name: School of Physics and Astronomy, Cardiff University, The Parade, Cardiff CF24 3AA, UK - index: 7 - -date: 25 January 2022 -codeRepository: https://github.com/PyAutoLabs/PyAutoLens -license: MIT -bibliography: paper.bib ---- - -# Summary - -Strong gravitational lensing, which can make a background source galaxy appears multiple times due to its light rays being -deflected by the mass of one or more foreground lens galaxies, provides astronomers with a powerful tool to study dark -matter, cosmology and the most distant Universe. `PyAutoLens` is an open-source Python 3.8 - 3.11 package for strong -gravitational lensing, with core features including fully automated strong lens modeling of galaxies and galaxy -clusters, support for direct imaging and interferometer datasets and comprehensive tools for simulating samples of -strong lenses. The API allows users to perform ray-tracing by using analytic light and mass profiles to build strong -lens systems. Accompanying `PyAutoLens` is the [autolens workspace](https://github.com/PyAutoLabs/autolens_workspace), which -includes example scripts and lens datasets covering every use case. The [`HowToLens`](https://github.com/PyAutoLabs/HowToLens) -repository provides a separate Jupyter notebook lecture series which introduces non-experts to strong lensing using `PyAutoLens`. Readers can +--- +title: "`PyAutoLens`: Open-Source Lens Modeling" +tags: + - astronomy + - galaxy formation and evolution + - galaxies + - interferometry + - Python +authors: + - name: James. W. Nightingale + orcid: 0000-0002-8987-7401 + affiliation: 1 + - name: Richard G. Hayes + affiliation: 1 + - name: Aristeidis Amvrosiadis + orcid: 0000-0002-4465-1564 + affiliation: 1 + - name: Ashley Kelly + orcid: 0000-0003-3850-4469 + affiliation: 1 + - name: Amy Etherington + affiliation: 1 + - name: Qiuhan He + orcid: 0000-0003-3672-9365 + affiliation: 1 + - name: Nan Li + orcid: 0000-0001-6800-7389 + affiliation: 2 + - name: XiaoYue Cao + affiliation: 3 + - name: Jonathan Frawley + orcid: 0000-0002-9437-7399 + affiliation: 4 + - name: Shaun Cole + orcid: 0000-0002-5954-7903 + affiliation: 1 + - name: Andrea Enia + orcid: 0000-0002-0200-2857 + affiliation: 5 + - name: Carlos S. Frenk + orcid: 0000-0002-2338-716X + affiliation: 1 + - name: David R. Harvey + orcid: 0000-0002-6066-6707 + affiliation: 6 + - name: Ran Li + orcid: 0000-0003-3899-0612 + affiliation: 3 + - name: Richard J. Massey + orcid: 0000-0002-6085-3780 + affiliation: 1 + - name: Mattia Negrello + orcid: 0000-0002-7925-7663 + affiliation: 7 + - name: Andrew Robertson + orcid: 0000-0002-0086-0524 + affiliation: 1 +affiliations: + - name: Institute for Computational Cosmology, Stockton Rd, Durham DH1 3LE + index: 1 + - name: Key Laboratory of Space Astronomy and Technology, National Astronomical Observatories, Chinese Academy of Sciences, Beijing 100101, China + index: 2 + - name: National Astronomical Observatories, Chinese Academy of Sciences, 20A Datun Road, Chaoyang District, Beijing 100012, China + index: 3 + - name: Advanced Research Computing, Durham University, Durham DH1 3LE + index: 4 + - name: Dipartimento di Fisica e Astronomia, Università degli Studi di Bologna, Via Berti Pichat 6/2, I-40127 Bologna, Italy + index: 5 + - name: Lorentz Institute, Leiden University, Niels Bohrweg 2, Leiden, NL-2333 CA, The Netherlands + index: 6 + - name: School of Physics and Astronomy, Cardiff University, The Parade, Cardiff CF24 3AA, UK + index: 7 + +date: 25 January 2022 +codeRepository: https://github.com/PyAutoLabs/PyAutoLens +license: MIT +bibliography: paper.bib +--- + +# Summary + +Strong gravitational lensing, which can make a background source galaxy appears multiple times due to its light rays being +deflected by the mass of one or more foreground lens galaxies, provides astronomers with a powerful tool to study dark +matter, cosmology and the most distant Universe. `PyAutoLens` is an open-source Python 3.8 - 3.11 package for strong +gravitational lensing, with core features including fully automated strong lens modeling of galaxies and galaxy +clusters, support for direct imaging and interferometer datasets and comprehensive tools for simulating samples of +strong lenses. The API allows users to perform ray-tracing by using analytic light and mass profiles to build strong +lens systems. Accompanying `PyAutoLens` is the [autolens workspace](https://github.com/PyAutoLabs/autolens_workspace), which +includes example scripts and lens datasets covering every use case. The [`HowToLens`](https://github.com/PyAutoLabs/HowToLens) +repository provides a separate Jupyter notebook lecture series which introduces non-experts to strong lensing using `PyAutoLens`. Readers can try `PyAutoLens` right now by going to [the introduction Jupyter notebook on Colab](https://colab.research.google.com/github/PyAutoLabs/autolens_workspace/blob/2026.7.25.2/start_here.ipynb) -or checkout the [readthedocs](https://pyautolens.readthedocs.io/en/latest/) for a complete overview of `PyAutoLens`'s features. - -# Background - -When two galaxies are aligned down the line-of-sight to Earth, light rays from the background galaxy are deflected by the -intervening mass of one or more foreground galaxies. Sometimes its light is fully bent around the foreground galaxies, -traversing multiple paths to the Earth, meaning that the background galaxy is observed multiple times. This alignment -of galaxies is called a strong gravitational lens, an example of which, SLACS1430+4105, is shown in the image -below. The massive elliptical lens galaxy can be seen in the centre of the left panel, surrounded by a multiply -imaged source galaxy whose light has been distorted into an Einstein ring. The central panel shows a `PyAutoLens` -reconstruction of the lensed source's light, where the foreground lens's light was simultaneously fitted for and -subtracted to reveal the source. The right panel shows a pixelized reconstruction of the source's unlensed light -distribution performed by `PyAutoLens`, which is created using a model of the lens galaxy's mass to trace backwards -how the source's light is gravitationally lensed. - -![Hubble Space Telescope imaging of the strong lens SLACSJ1430+1405 (left column), a fit to its lensed source galaxy (middle column) and unlensed source reconstruction (right column) using `PyAutoLens`.\label{figure:example}](imageaxis.png) - -Strong lensing provides astronomers with an invaluable tool to study a diverse range of topics. Mass modeling of -strong lenses has quantified the distribution of stars [@Koopmans2009] [@Sonnenfeld2015] [@Treu2009] [@Nightingale2019] -and invisible dark matter [@Vegetti2014] of galaxies. The source galaxy is highly magnified and reconstruction of -its light allows a view of fainter or more distant objects than would otherwise be possible [@Dye2014] [@Enia2018]. -Strong lensing is a competitive test of cosmological models, for example the expansion rate of the Universe can -be inferred from the 'time-delay' between different image paths to the same distant quasar [@Suyu2016]. Strong lensing -of galaxy clusters has also made many contributions to all these topics [@Jullo2010] [@Richard2014] [@Atek2015]. - -# Statement of Need - -The past decade has seen the discovery of many hundreds of galaxy-scale and cluster-scale lenses, with high quality -imaging [@Bolton2012], interferometer [@Negrello2014] [@Enia2018] and spectroscopy [@Czoske2012] datasets now available. -Historically, the modeling of a strong lens is a time-intensive process that requires significant human intervention -to perform, restricting the scope and size of the scientific analysis. In the next decade of -order of _one hundred thousand_ strong lenses will be discovered by surveys such as Euclid, LSST and -SKA [@Collett2015], demanding a widely available and automated approach for strong lens analysis. `PyAutoLens` aims to -meet this need, by making strong lens analysis accessible to the wider Astronomy community and enabling the automated -analysis of large samples of strong lenses. - -# Software API and Features - -A gravitational lens system can be quickly assembled from Python objects which provide abstract data representations -of the different components of a strong lens. A `Galaxy` object contains one or more `LightProfile`'s and `MassProfile`'s, -which represent its two dimensional distribution of starlight and mass. `Galaxy`’s lie at a particular distance -(redshift) from the observer, and are grouped into `Plane`'s. Raytracing through multiple `Plane`s is achieved by -passing them to a `Tracer` with an `astropy` Cosmology. By passing any of these objects a `Grid2D` object strong lens -quantities can be computed, including multi-plane ray-tracing sightlines [@McCully2014]. All of these objects are -extensible, making it straightforward to compose highly customized lensing system. Ray-tracing calculations are -optimized using the packages `NumPy` [@numpy], `numba` [@numba] and `pyquad` [@pyquad]. - -To perform lens modeling, `PyAutoLens` adopts the probabilistic programming -language `PyAutoFit` (https://github.com/PyAutoLabs/PyAutoFit). `PyAutoFit` allows users to compose a -lens model from `LightProfile`, `MassProfile` and `Galaxy` objects, customize the model parameterization and fit it to -data via a non-linear search (e.g., `dynesty` [@dynesty], `emcee` [@emcee], `PySwarms` [@pyswarms]). By composing a -lens model with a `Pixelization` object, the background source's light is reconstructed using a -rectangular grid or Voronoi mesh that accounts for irregular galaxy morphologies. Lensed quasar and supernovae datasets -can be fitted using a `Point`, which uses their observed positions, flux-ratios and time-delays to fit the lens -model. Strong lensing clusters consisting of any number of lens galaxies can also be analysed with `PyAutoLens` using these objects. - -Automated lens modeling uses `PyAutoFit`'s non-linear search chaining feature, which breaks the model-fit into -a chained sequence of non-linear searches. These fits pass information gained about simpler lens models fitted by earlier -searches to subsequent searches, which fit progressively more complex models. By granularizing the model-fitting -procedure, automated pipelines that fit complex lens models without human intervention can be carefully crafted, with -example pipelines found on the [autolens workspace](https://github.com/PyAutoLabs/autolens_workspace). To ensure the -analysis and interpretation of fits to large lens datasets is feasible, `PyAutoFit`'s database tools write lens modeling -results to a relational database which can be queried from hard-disk to a Python script or Jupyter notebook. This uses -memory-light `Python` generators, ensuring it is practical for thousands of lenses. - -`PyAutoLens` includes a comprehensive visualization library for the analysis of both direct imaging and submm / radio -interferometer datasets, tools for preprocessing data to formats suited to lens analysis and options to include -effects like the telescope optics and background sky subtraction in the model-fit. Interferometer analysis is -performed directly on the observed visibilities in their native Fourier space, circumventing issues associated with the -incomplete sampling of the uv-plane that give rise to artefacts that can bias the inferred mass model and source -reconstruction in real-space. To make feasible the analysis of millions of visibilities, `PyAutoLens` -uses `PyNUFFT` [@pynufft] to fit the visibilities via a non-uniform fast Fourier transform. Creating -realistic simulations of imaging and interferometer strong lensing datasets is possible, as performed -by [@Alexander2019] [@Hermans2019] who used `PyAutoLens` to train neural networks to detect strong lenses. - -# Performance - -The analysis of direct imaging datasets and interferometer datasets (up to of order 1 million visibilities) are both -feasible on hardware with at least 4GB of RAM. The time it takes to perform lens modeling with `PyAutoLens` is -highly variable and depends on the size of the dataset being analysed and complexity of the model being fitted. They can -vary from minutes to thousands of CPU hours. The run-times section on [readthedocs](https://pyautolens.readthedocs.io/en/latest/) -provides graphs showing the performance of the latest release of `PyAutoLens` and a calculator for estimating how long -a lens model fit may take. For large jobs we recommend users install `PyAutoLens` on a HPC cluster and documentation is -provided on how to set this up. - -# Workspace and HowToLens Tutorials - -`PyAutoLens` is distributed with the [autolens workspace](https://github.com/PyAutoLabs/autolens_workspace), which -contains example scripts for modeling and simulating strong lenses and tutorials on how to preprocess imaging and -interferometer datasets before a `PyAutoLens` analysis. The [`HowToLens`](https://github.com/PyAutoLabs/HowToLens) -tutorials — a standalone repository separate from the workspace — are a five chapter lecture series composed of over -30 Jupyter notebooks aimed at non-experts, introducing them to strong gravitational lensing, Bayesian inference and -teaching them how to use `PyAutoLens` for their scientific study. The lectures are available on -[Colab](https://colab.research.google.com/github/PyAutoLabs/HowToLens/blob/2026.7.25.2/notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb) -and may therefore be taken without a local `PyAutoLens` installation. - -# Software Citations - -`PyAutoLens` is written in Python 3.8 - 3.11 [@python] and uses the following software packages: - -- `Astropy` [@astropy1] [@astropy2] -- `COLOSSUS` [@colossus] -- `corner.py` [@corner] -- `dynesty` [@dynesty] -- `emcee` [@emcee] -- `Matplotlib` [@matplotlib] -- `numba` [@numba] -- `NumPy` [@numpy] -- `PyAutoFit` [@pyautofit] -- `PyMultiNest` [@pymultinest] [@multinest] -- `PyNUFFT` [@pynufft] -- `pyprojroot` (https://github.com/chendaniely/pyprojroot) -- `pyquad` [@pyquad] -- `PySwarms` [@pyswarms] -- `scikit-image` [@scikit-image] -- `scikit-learn` [@scikit-learn] -- `Scipy` [@scipy] - -# Related Software - -- `AutoLens` [@Nightingale2015] [@Nightingale2018] -- `gravlens` http://www.physics.rutgers.edu/~keeton/gravlens/manual.pdf -- `lenstronomy` https://github.com/sibirrer/lenstronomy [@Birrer2018a] -- `visilens` https://github.com/jspilker/visilens [@spilker16a] - -# Acknowledgements - -JWN and RJM are supported by the UK Space Agency, through grant ST/V001582/1, and by InnovateUK through grant TS/V002856/1. RGH is supported by STFC Opportunities grant ST/T002565/1. -QH, CSF and SMC are supported by ERC Advanced In-vestigator grant, DMIDAS [GA 786910] and also by the STFCConsolidated Grant for Astronomy at Durham [grant numbersST/F001166/1, ST/I00162X/1,ST/P000541/1]. -RJM is supported by a Royal Society University Research Fellowship. -DH acknowledges support by the ITP Delta foundation. -AR is supported bythe ERC Horizon2020 project ‘EWC’ (award AMD-776247-6). -MN has received funding from the European Union’s Horizon 2020 research and innovation programme under the Marie Sklodowska-Curie grant agreement no. 707601. -This work used the DiRAC@Durham facility managed by the Institute for Computational Cosmology on behalf of the STFC DiRAC HPC Facility (www.dirac.ac.uk). The equipment was funded by BEIS capital funding via STFC capital grants ST/K00042X/1, ST/P002293/1, ST/R002371/1 and ST/S002502/1, Durham University and STFC operations grant ST/R000832/1. DiRAC is part of the National e-Infrastructure. - -# References +or checkout the [readthedocs](https://pyautolens.readthedocs.io/en/latest/) for a complete overview of `PyAutoLens`'s features. + +# Background + +When two galaxies are aligned down the line-of-sight to Earth, light rays from the background galaxy are deflected by the +intervening mass of one or more foreground galaxies. Sometimes its light is fully bent around the foreground galaxies, +traversing multiple paths to the Earth, meaning that the background galaxy is observed multiple times. This alignment +of galaxies is called a strong gravitational lens, an example of which, SLACS1430+4105, is shown in the image +below. The massive elliptical lens galaxy can be seen in the centre of the left panel, surrounded by a multiply +imaged source galaxy whose light has been distorted into an Einstein ring. The central panel shows a `PyAutoLens` +reconstruction of the lensed source's light, where the foreground lens's light was simultaneously fitted for and +subtracted to reveal the source. The right panel shows a pixelized reconstruction of the source's unlensed light +distribution performed by `PyAutoLens`, which is created using a model of the lens galaxy's mass to trace backwards +how the source's light is gravitationally lensed. + +![Hubble Space Telescope imaging of the strong lens SLACSJ1430+1405 (left column), a fit to its lensed source galaxy (middle column) and unlensed source reconstruction (right column) using `PyAutoLens`.\label{figure:example}](imageaxis.png) + +Strong lensing provides astronomers with an invaluable tool to study a diverse range of topics. Mass modeling of +strong lenses has quantified the distribution of stars [@Koopmans2009] [@Sonnenfeld2015] [@Treu2009] [@Nightingale2019] +and invisible dark matter [@Vegetti2014] of galaxies. The source galaxy is highly magnified and reconstruction of +its light allows a view of fainter or more distant objects than would otherwise be possible [@Dye2014] [@Enia2018]. +Strong lensing is a competitive test of cosmological models, for example the expansion rate of the Universe can +be inferred from the 'time-delay' between different image paths to the same distant quasar [@Suyu2016]. Strong lensing +of galaxy clusters has also made many contributions to all these topics [@Jullo2010] [@Richard2014] [@Atek2015]. + +# Statement of Need + +The past decade has seen the discovery of many hundreds of galaxy-scale and cluster-scale lenses, with high quality +imaging [@Bolton2012], interferometer [@Negrello2014] [@Enia2018] and spectroscopy [@Czoske2012] datasets now available. +Historically, the modeling of a strong lens is a time-intensive process that requires significant human intervention +to perform, restricting the scope and size of the scientific analysis. In the next decade of +order of _one hundred thousand_ strong lenses will be discovered by surveys such as Euclid, LSST and +SKA [@Collett2015], demanding a widely available and automated approach for strong lens analysis. `PyAutoLens` aims to +meet this need, by making strong lens analysis accessible to the wider Astronomy community and enabling the automated +analysis of large samples of strong lenses. + +# Software API and Features + +A gravitational lens system can be quickly assembled from Python objects which provide abstract data representations +of the different components of a strong lens. A `Galaxy` object contains one or more `LightProfile`'s and `MassProfile`'s, +which represent its two dimensional distribution of starlight and mass. `Galaxy`’s lie at a particular distance +(redshift) from the observer, and are grouped into `Plane`'s. Raytracing through multiple `Plane`s is achieved by +passing them to a `Tracer` with an `astropy` Cosmology. By passing any of these objects a `Grid2D` object strong lens +quantities can be computed, including multi-plane ray-tracing sightlines [@McCully2014]. All of these objects are +extensible, making it straightforward to compose highly customized lensing system. Ray-tracing calculations are +optimized using the packages `NumPy` [@numpy], `numba` [@numba] and `pyquad` [@pyquad]. + +To perform lens modeling, `PyAutoLens` adopts the probabilistic programming +language `PyAutoFit` (https://github.com/PyAutoLabs/PyAutoFit). `PyAutoFit` allows users to compose a +lens model from `LightProfile`, `MassProfile` and `Galaxy` objects, customize the model parameterization and fit it to +data via a non-linear search (e.g., `dynesty` [@dynesty], `emcee` [@emcee], `PySwarms` [@pyswarms]). By composing a +lens model with a `Pixelization` object, the background source's light is reconstructed using a +rectangular grid or Voronoi mesh that accounts for irregular galaxy morphologies. Lensed quasar and supernovae datasets +can be fitted using a `Point`, which uses their observed positions, flux-ratios and time-delays to fit the lens +model. Strong lensing clusters consisting of any number of lens galaxies can also be analysed with `PyAutoLens` using these objects. + +Automated lens modeling uses `PyAutoFit`'s non-linear search chaining feature, which breaks the model-fit into +a chained sequence of non-linear searches. These fits pass information gained about simpler lens models fitted by earlier +searches to subsequent searches, which fit progressively more complex models. By granularizing the model-fitting +procedure, automated pipelines that fit complex lens models without human intervention can be carefully crafted, with +example pipelines found on the [autolens workspace](https://github.com/PyAutoLabs/autolens_workspace). To ensure the +analysis and interpretation of fits to large lens datasets is feasible, `PyAutoFit`'s database tools write lens modeling +results to a relational database which can be queried from hard-disk to a Python script or Jupyter notebook. This uses +memory-light `Python` generators, ensuring it is practical for thousands of lenses. + +`PyAutoLens` includes a comprehensive visualization library for the analysis of both direct imaging and submm / radio +interferometer datasets, tools for preprocessing data to formats suited to lens analysis and options to include +effects like the telescope optics and background sky subtraction in the model-fit. Interferometer analysis is +performed directly on the observed visibilities in their native Fourier space, circumventing issues associated with the +incomplete sampling of the uv-plane that give rise to artefacts that can bias the inferred mass model and source +reconstruction in real-space. To make feasible the analysis of millions of visibilities, `PyAutoLens` +uses `PyNUFFT` [@pynufft] to fit the visibilities via a non-uniform fast Fourier transform. Creating +realistic simulations of imaging and interferometer strong lensing datasets is possible, as performed +by [@Alexander2019] [@Hermans2019] who used `PyAutoLens` to train neural networks to detect strong lenses. + +# Performance + +The analysis of direct imaging datasets and interferometer datasets (up to of order 1 million visibilities) are both +feasible on hardware with at least 4GB of RAM. The time it takes to perform lens modeling with `PyAutoLens` is +highly variable and depends on the size of the dataset being analysed and complexity of the model being fitted. They can +vary from minutes to thousands of CPU hours. The run-times section on [readthedocs](https://pyautolens.readthedocs.io/en/latest/) +provides graphs showing the performance of the latest release of `PyAutoLens` and a calculator for estimating how long +a lens model fit may take. For large jobs we recommend users install `PyAutoLens` on a HPC cluster and documentation is +provided on how to set this up. + +# Workspace and HowToLens Tutorials + +`PyAutoLens` is distributed with the [autolens workspace](https://github.com/PyAutoLabs/autolens_workspace), which +contains example scripts for modeling and simulating strong lenses and tutorials on how to preprocess imaging and +interferometer datasets before a `PyAutoLens` analysis. The [`HowToLens`](https://github.com/PyAutoLabs/HowToLens) +tutorials — a standalone repository separate from the workspace — are a five chapter lecture series composed of over +30 Jupyter notebooks aimed at non-experts, introducing them to strong gravitational lensing, Bayesian inference and +teaching them how to use `PyAutoLens` for their scientific study. The lectures are available on +[Colab](https://colab.research.google.com/github/PyAutoLabs/HowToLens/blob/2026.7.25.2/notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb) +and may therefore be taken without a local `PyAutoLens` installation. + +# Software Citations + +`PyAutoLens` is written in Python 3.8 - 3.11 [@python] and uses the following software packages: + +- `Astropy` [@astropy1] [@astropy2] +- `COLOSSUS` [@colossus] +- `corner.py` [@corner] +- `dynesty` [@dynesty] +- `emcee` [@emcee] +- `Matplotlib` [@matplotlib] +- `numba` [@numba] +- `NumPy` [@numpy] +- `PyAutoFit` [@pyautofit] +- `PyMultiNest` [@pymultinest] [@multinest] +- `PyNUFFT` [@pynufft] +- `pyprojroot` (https://github.com/chendaniely/pyprojroot) +- `pyquad` [@pyquad] +- `PySwarms` [@pyswarms] +- `scikit-image` [@scikit-image] +- `scikit-learn` [@scikit-learn] +- `Scipy` [@scipy] + +# Related Software + +- `AutoLens` [@Nightingale2015] [@Nightingale2018] +- `gravlens` http://www.physics.rutgers.edu/~keeton/gravlens/manual.pdf +- `lenstronomy` https://github.com/sibirrer/lenstronomy [@Birrer2018a] +- `visilens` https://github.com/jspilker/visilens [@spilker16a] + +# Acknowledgements + +JWN and RJM are supported by the UK Space Agency, through grant ST/V001582/1, and by InnovateUK through grant TS/V002856/1. RGH is supported by STFC Opportunities grant ST/T002565/1. +QH, CSF and SMC are supported by ERC Advanced In-vestigator grant, DMIDAS [GA 786910] and also by the STFCConsolidated Grant for Astronomy at Durham [grant numbersST/F001166/1, ST/I00162X/1,ST/P000541/1]. +RJM is supported by a Royal Society University Research Fellowship. +DH acknowledges support by the ITP Delta foundation. +AR is supported bythe ERC Horizon2020 project ‘EWC’ (award AMD-776247-6). +MN has received funding from the European Union’s Horizon 2020 research and innovation programme under the Marie Sklodowska-Curie grant agreement no. 707601. +This work used the DiRAC@Durham facility managed by the Institute for Computational Cosmology on behalf of the STFC DiRAC HPC Facility (www.dirac.ac.uk). The equipment was funded by BEIS capital funding via STFC capital grants ST/K00042X/1, ST/P002293/1, ST/R002371/1 and ST/S002502/1, Durham University and STFC operations grant ST/R000832/1. DiRAC is part of the National e-Infrastructure. + +# References diff --git a/pyproject.toml b/pyproject.toml index 9c1c91ff5..a27248fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,78 +1,78 @@ -[build-system] -requires = ["setuptools>=79.0", "setuptools-scm", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "autolens" -dynamic = ["version"] -description="Open-Source Strong Lensing" -readme = { file = "README.md", content-type = "text/markdown" } -license = { text = "MIT" } -requires-python = ">=3.9" -authors = [ - { name = "James Nightingale", email = "James.Nightingale@newcastle.ac.uk" }, - { name = "Richard Hayes", email = "richard@rghsoftware.co.uk" }, -] -classifiers = [ - "Intended Audience :: Science/Research", - "Topic :: Scientific/Engineering :: Physics", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13" -] -keywords = ["cli"] -dependencies = [ - "autogalaxy", - "nautilus-sampler==1.0.5" -] - -[project.urls] -Homepage = "https://github.com/PyAutoLabs/PyAutoLens" - -[tool.setuptools] -include-package-data = true - -[tool.setuptools.packages.find] -exclude = ["docs", "test_autolens", "test_autolens*"] - -[tool.setuptools_scm] -version_scheme = "post-release" -local_scheme = "no-local-version" - - -[project.optional-dependencies] -jax = ["autogalaxy[jax]"] -coolest = ["coolest"] -optional = [ - "autolens[jax]", - "coolest", - "numba", - "pynufft", - "zeus-mcmc==2.5.4", - "getdist==1.4" -] -docs=[ - "sphinx", - "furo", - "myst-parser", - "sphinx_copybutton", - "sphinx_design", - "sphinx_inline_tabs", - "sphinx_autodoc_typehints" -] -test = ["pytest", "coolest"] -dev = ["pytest", "black", "coolest"] - -[tool.setuptools.package-data] -"autolens.config" = ["*"] - -[tool.pytest.ini_options] -testpaths = ["test_autolens"] -filterwarnings = [ - "ignore:cuda_plugin_extension:UserWarning", - "ignore::DeprecationWarning:jax", +[build-system] +requires = ["setuptools>=79.0", "setuptools-scm", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "autolens" +dynamic = ["version"] +description="Open-Source Strong Lensing" +readme = { file = "README.md", content-type = "text/markdown" } +license = { text = "MIT" } +requires-python = ">=3.9" +authors = [ + { name = "James Nightingale", email = "James.Nightingale@newcastle.ac.uk" }, + { name = "Richard Hayes", email = "richard@rghsoftware.co.uk" }, +] +classifiers = [ + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Physics", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13" +] +keywords = ["cli"] +dependencies = [ + "autogalaxy", + "nautilus-sampler==1.0.5" +] + +[project.urls] +Homepage = "https://github.com/PyAutoLabs/PyAutoLens" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +exclude = ["docs", "test_autolens", "test_autolens*"] + +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "no-local-version" + + +[project.optional-dependencies] +jax = ["autogalaxy[jax]"] +coolest = ["coolest"] +optional = [ + "autolens[jax]", + "coolest", + "numba", + "pynufft", + "zeus-mcmc==2.5.4", + "getdist==1.4" +] +docs=[ + "sphinx", + "furo", + "myst-parser", + "sphinx_copybutton", + "sphinx_design", + "sphinx_inline_tabs", + "sphinx_autodoc_typehints" +] +test = ["pytest", "coolest"] +dev = ["pytest", "black", "coolest"] + +[tool.setuptools.package-data] +"autolens.config" = ["*"] + +[tool.pytest.ini_options] +testpaths = ["test_autolens"] +filterwarnings = [ + "ignore:cuda_plugin_extension:UserWarning", + "ignore::DeprecationWarning:jax", ] \ No newline at end of file diff --git a/setup.py b/setup.py index 4c77d449f..1c6dbc092 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ -import os -from setuptools import setup - -version = os.environ.get("VERSION", "1.0.dev0") - -setup( - version=version, -) +import os +from setuptools import setup + +version = os.environ.get("VERSION", "1.0.dev0") + +setup( + version=version, +) diff --git a/test_autolens/analysis/analysis/test_analysis_dataset.py b/test_autolens/analysis/analysis/test_analysis_dataset.py index 04d125e5d..b38cc1e80 100644 --- a/test_autolens/analysis/analysis/test_analysis_dataset.py +++ b/test_autolens/analysis/analysis/test_analysis_dataset.py @@ -1,120 +1,120 @@ -from pathlib import Path -import importlib.util -import os -import pytest - -from autonerves import conf -from autonerves.dictable import from_json - -import autofit as af -import autolens as al -from autolens import exc - -directory = Path(__file__).resolve().parent - - -def _jax_installed() -> bool: - return importlib.util.find_spec("jax") is not None - - -def test__pyauto_disable_jax_env_downgrades_use_jax__imaging( - monkeypatch, masked_imaging_7x7 -): - # Regression cover for the deleted local env read in `AnalysisDataset`: - # the disable-jax env var must still downgrade `use_jax`, now resolved - # solely by `af.Analysis.__init__` (the single reader) and forwarded to - # `AnalysisLens` as `self._use_jax`. - monkeypatch.setenv("PYAUTO_DISABLE_JAX", "1") - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=True) - - assert analysis._use_jax is False - - -@pytest.mark.skipif(not _jax_installed(), reason="jax not installed") -def test__use_jax_true_env_unset__not_downgraded__imaging( - monkeypatch, masked_imaging_7x7 -): - # No over-downgrade: with the env var unset and jax installed, - # `use_jax=True` must survive as `self._use_jax is True`. - monkeypatch.delenv("PYAUTO_DISABLE_JAX", raising=False) - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=True) - - assert analysis._use_jax is True - - -def test__modify_before_fit__inversion_no_positions_likelihood__raises_exception( - masked_imaging_7x7, -): - lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph()) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(), regularization=al.reg.Constant() - ) - - source = al.Galaxy(redshift=1.0, pixelization=pixelization) - - model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) - - with pytest.raises(exc.AnalysisException): - analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) - - positions_likelihood = al.PositionsLH( - positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 - ) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, - positions_likelihood_list=[positions_likelihood], - use_jax=False, - ) - analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) - - -def test__save_results__tracer_output_to_json(analysis_imaging_7x7): - lens = al.Galaxy(redshift=0.5) - source = al.Galaxy(redshift=1.0) - - model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) - - tracer = al.Tracer(galaxies=[lens, source]) - - paths = af.DirectoryPaths() - - analysis_imaging_7x7.save_results( - paths=paths, - result=al.m.MockResult(max_log_likelihood_tracer=tracer, model=model), - ) - - tracer = from_json(file_path=paths._files_path / "tracer.json") - - assert tracer.galaxies[0].redshift == 0.5 - assert tracer.galaxies[1].redshift == 1.0 - - os.remove(paths._files_path / "tracer.json") - - -def test__save_attributes__dataset_fits_output_for_aggregator(analysis_imaging_7x7): - # Regression guard: `save_attributes` must always write `dataset.fits` to the - # `files` folder so the aggregator loaders (`ImagingAgg`, - # `agg_util.mask_header_from`) can reload the dataset via - # `fit.value(name="dataset")`, independently of whether visualization ran. - from astropy.io import fits - - paths = af.DirectoryPaths() - - analysis_imaging_7x7.save_attributes(paths=paths) - - dataset_fits_path = paths._files_path / "dataset.fits" - - assert dataset_fits_path.exists() - - with fits.open(dataset_fits_path) as hdu_list: - ext_names = [hdu.name for hdu in hdu_list] - - assert ext_names[:4] == ["MASK", "DATA", "NOISE_MAP", "PSF"] - - os.remove(dataset_fits_path) +from pathlib import Path +import importlib.util +import os +import pytest + +from autonerves import conf +from autonerves.dictable import from_json + +import autofit as af +import autolens as al +from autolens import exc + +directory = Path(__file__).resolve().parent + + +def _jax_installed() -> bool: + return importlib.util.find_spec("jax") is not None + + +def test__pyauto_disable_jax_env_downgrades_use_jax__imaging( + monkeypatch, masked_imaging_7x7 +): + # Regression cover for the deleted local env read in `AnalysisDataset`: + # the disable-jax env var must still downgrade `use_jax`, now resolved + # solely by `af.Analysis.__init__` (the single reader) and forwarded to + # `AnalysisLens` as `self._use_jax`. + monkeypatch.setenv("PYAUTO_DISABLE_JAX", "1") + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=True) + + assert analysis._use_jax is False + + +@pytest.mark.skipif(not _jax_installed(), reason="jax not installed") +def test__use_jax_true_env_unset__not_downgraded__imaging( + monkeypatch, masked_imaging_7x7 +): + # No over-downgrade: with the env var unset and jax installed, + # `use_jax=True` must survive as `self._use_jax is True`. + monkeypatch.delenv("PYAUTO_DISABLE_JAX", raising=False) + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=True) + + assert analysis._use_jax is True + + +def test__modify_before_fit__inversion_no_positions_likelihood__raises_exception( + masked_imaging_7x7, +): + lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph()) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(), regularization=al.reg.Constant() + ) + + source = al.Galaxy(redshift=1.0, pixelization=pixelization) + + model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) + + with pytest.raises(exc.AnalysisException): + analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) + + positions_likelihood = al.PositionsLH( + positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 + ) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, + positions_likelihood_list=[positions_likelihood], + use_jax=False, + ) + analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) + + +def test__save_results__tracer_output_to_json(analysis_imaging_7x7): + lens = al.Galaxy(redshift=0.5) + source = al.Galaxy(redshift=1.0) + + model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) + + tracer = al.Tracer(galaxies=[lens, source]) + + paths = af.DirectoryPaths() + + analysis_imaging_7x7.save_results( + paths=paths, + result=al.m.MockResult(max_log_likelihood_tracer=tracer, model=model), + ) + + tracer = from_json(file_path=paths._files_path / "tracer.json") + + assert tracer.galaxies[0].redshift == 0.5 + assert tracer.galaxies[1].redshift == 1.0 + + os.remove(paths._files_path / "tracer.json") + + +def test__save_attributes__dataset_fits_output_for_aggregator(analysis_imaging_7x7): + # Regression guard: `save_attributes` must always write `dataset.fits` to the + # `files` folder so the aggregator loaders (`ImagingAgg`, + # `agg_util.mask_header_from`) can reload the dataset via + # `fit.value(name="dataset")`, independently of whether visualization ran. + from astropy.io import fits + + paths = af.DirectoryPaths() + + analysis_imaging_7x7.save_attributes(paths=paths) + + dataset_fits_path = paths._files_path / "dataset.fits" + + assert dataset_fits_path.exists() + + with fits.open(dataset_fits_path) as hdu_list: + ext_names = [hdu.name for hdu in hdu_list] + + assert ext_names[:4] == ["MASK", "DATA", "NOISE_MAP", "PSF"] + + os.remove(dataset_fits_path) diff --git a/test_autolens/analysis/analysis/test_analysis_lens.py b/test_autolens/analysis/analysis/test_analysis_lens.py index 0220dd162..754b49868 100644 --- a/test_autolens/analysis/analysis/test_analysis_lens.py +++ b/test_autolens/analysis/analysis/test_analysis_lens.py @@ -1,72 +1,72 @@ -from pathlib import Path -import pytest - -import autofit as af -import autolens as al - -directory = Path(__file__).resolve().parent - - -def test__tracer_for_instance(analysis_imaging_7x7): - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, - light=al.lp.SersicSph(intensity=2.0), - mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ), - source=al.Galaxy(redshift=1.0), - ) - + af.Collection( - extra_galaxyp=al.Galaxy( - redshift=0.5, - light=al.lp.SersicSph(intensity=0.1), - mass=al.mp.IsothermalSph(einstein_radius=0.2), - ) - ), - ) - - instance = model.instance_from_unit_vector([]) - tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) - - assert tracer.galaxies[0].redshift == 0.5 - assert tracer.galaxies[0].light.intensity == 2.0 - assert tracer.galaxies[0].mass.centre == pytest.approx((0.0, 0.0), 1.0e-4) - assert tracer.galaxies[0].mass.einstein_radius == 1.0 - assert tracer.galaxies[2].redshift == 0.5 - assert tracer.galaxies[2].light.intensity == 0.1 - assert tracer.galaxies[2].mass.einstein_radius == 0.2 - - -def test__tracer_for_instance__subhalo_redshift_rescale_used(analysis_imaging_7x7): - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, - mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ), - subhalo=al.Galaxy(redshift=0.25, mass=al.mp.NFWSph(centre=(0.1, 0.2))), - source=al.Galaxy(redshift=1.0), - ) - ) - - instance = model.instance_from_unit_vector([]) - tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) - - assert tracer.galaxies[1].mass.centre == pytest.approx((0.1, 0.2), 1.0e-4) - - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, - mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ), - subhalo=al.Galaxy(redshift=0.75, mass=al.mp.NFWSph(centre=(0.1, 0.2))), - source=al.Galaxy(redshift=1.0), - ) - ) - - instance = model.instance_from_unit_vector([]) - tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) - - assert tracer.galaxies[1].mass.centre == pytest.approx((-0.19959, -0.39919), 1.0e-4) +from pathlib import Path +import pytest + +import autofit as af +import autolens as al + +directory = Path(__file__).resolve().parent + + +def test__tracer_for_instance(analysis_imaging_7x7): + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, + light=al.lp.SersicSph(intensity=2.0), + mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ), + source=al.Galaxy(redshift=1.0), + ) + + af.Collection( + extra_galaxyp=al.Galaxy( + redshift=0.5, + light=al.lp.SersicSph(intensity=0.1), + mass=al.mp.IsothermalSph(einstein_radius=0.2), + ) + ), + ) + + instance = model.instance_from_unit_vector([]) + tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) + + assert tracer.galaxies[0].redshift == 0.5 + assert tracer.galaxies[0].light.intensity == 2.0 + assert tracer.galaxies[0].mass.centre == pytest.approx((0.0, 0.0), 1.0e-4) + assert tracer.galaxies[0].mass.einstein_radius == 1.0 + assert tracer.galaxies[2].redshift == 0.5 + assert tracer.galaxies[2].light.intensity == 0.1 + assert tracer.galaxies[2].mass.einstein_radius == 0.2 + + +def test__tracer_for_instance__subhalo_redshift_rescale_used(analysis_imaging_7x7): + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, + mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ), + subhalo=al.Galaxy(redshift=0.25, mass=al.mp.NFWSph(centre=(0.1, 0.2))), + source=al.Galaxy(redshift=1.0), + ) + ) + + instance = model.instance_from_unit_vector([]) + tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) + + assert tracer.galaxies[1].mass.centre == pytest.approx((0.1, 0.2), 1.0e-4) + + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, + mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ), + subhalo=al.Galaxy(redshift=0.75, mass=al.mp.NFWSph(centre=(0.1, 0.2))), + source=al.Galaxy(redshift=1.0), + ) + ) + + instance = model.instance_from_unit_vector([]) + tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) + + assert tracer.galaxies[1].mass.centre == pytest.approx((-0.19959, -0.39919), 1.0e-4) diff --git a/test_autolens/analysis/test_analysis.py b/test_autolens/analysis/test_analysis.py index d20565a3a..157af729e 100644 --- a/test_autolens/analysis/test_analysis.py +++ b/test_autolens/analysis/test_analysis.py @@ -1,177 +1,177 @@ -import numpy as np -from pathlib import Path -import os -import pytest - -from autonerves import conf -from autonerves.dictable import from_json - -import autofit as af -import autolens as al -from autolens import exc - -directory = Path(__file__).resolve().parent - - -def test__tracer_for_instance(analysis_imaging_7x7): - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, - light=al.lp.SersicSph(intensity=2.0), - mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ), - source=al.Galaxy(redshift=1.0), - ) - + af.Collection( - extra_galaxy=al.Galaxy( - redshift=0.5, - light=al.lp.SersicSph(intensity=0.1), - mass=al.mp.IsothermalSph(einstein_radius=0.2), - ) - ), - ) - - instance = model.instance_from_unit_vector([]) - tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) - - assert tracer.galaxies[0].redshift == 0.5 - assert tracer.galaxies[0].light.intensity == 2.0 - assert tracer.galaxies[0].mass.centre == pytest.approx((0.0, 0.0), 1.0e-4) - assert tracer.galaxies[0].mass.einstein_radius == 1.0 - assert tracer.galaxies[2].redshift == 0.5 - assert tracer.galaxies[2].light.intensity == 0.1 - assert tracer.galaxies[2].mass.einstein_radius == 0.2 - - -def test__tracer_for_instance__subhalo_redshift_rescale_used(analysis_imaging_7x7): - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, - mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ), - subhalo=al.Galaxy(redshift=0.25, mass=al.mp.NFWSph(centre=(0.1, 0.2))), - source=al.Galaxy(redshift=1.0), - ) - ) - - instance = model.instance_from_unit_vector([]) - tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) - - assert tracer.galaxies[1].mass.centre == pytest.approx((0.1, 0.2), 1.0e-4) - - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, - mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ), - subhalo=al.Galaxy(redshift=0.75, mass=al.mp.NFWSph(centre=(0.1, 0.2))), - source=al.Galaxy(redshift=1.0), - ) - ) - - instance = model.instance_from_unit_vector([]) - tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) - - assert tracer.galaxies[1].mass.centre == pytest.approx((-0.19959, -0.39919), 1.0e-4) - - -def test__use_border_relocator__determines_if_border_pixel_relocation_is_used( - masked_imaging_7x7, -): - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=1.0), - ) - - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, mass=al.mp.IsothermalSph(einstein_radius=100.0) - ), - source=al.Galaxy(redshift=1.0, pixelization=pixelization), - ) - ) - - masked_imaging_7x7.grids.lp.over_sampled[4] = np.array([300.0, 0.0]) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, - settings=al.Settings(use_border_relocator=False), - use_jax=False, - ) - - instance = model.instance_from_unit_vector([]) - fit = analysis.fit_from(instance=instance) - - grid = fit.inversion.linear_obj_list[0].source_plane_data_grid.over_sampled - - assert grid[2] == pytest.approx([-82.99114877, 52.81254922], 1.0e-4) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, - settings=al.Settings(use_border_relocator=True), - use_jax=False, - ) - - instance = model.instance_from_unit_vector([]) - fit = analysis.fit_from(instance=instance) - - grid = fit.inversion.linear_obj_list[0].source_plane_data_grid.over_sampled - - assert grid[2] == pytest.approx([-82.991148773, 52.81254921], 1.0e-4) - - -def test__modify_before_fit__inversion_no_positions_likelihood__raises_exception( - masked_imaging_7x7, -): - lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph()) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(), regularization=al.reg.Constant() - ) - - source = al.Galaxy(redshift=1.0, pixelization=pixelization) - - model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) - - with pytest.raises(exc.AnalysisException): - analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) - - positions_likelihood = al.PositionsLH( - positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 - ) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, - positions_likelihood_list=[positions_likelihood], - use_jax=False, - ) - analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) - - -def test__save_results__tracer_output_to_json(analysis_imaging_7x7): - lens = al.Galaxy(redshift=0.5) - source = al.Galaxy(redshift=1.0) - - model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) - - tracer = al.Tracer(galaxies=[lens, source]) - - paths = af.DirectoryPaths() - - analysis_imaging_7x7.save_results( - paths=paths, - result=al.m.MockResult(max_log_likelihood_tracer=tracer, model=model), - ) - - tracer = from_json(file_path=paths._files_path / "tracer.json") - - assert tracer.galaxies[0].redshift == 0.5 - assert tracer.galaxies[1].redshift == 1.0 - - os.remove(paths._files_path / "tracer.json") +import numpy as np +from pathlib import Path +import os +import pytest + +from autonerves import conf +from autonerves.dictable import from_json + +import autofit as af +import autolens as al +from autolens import exc + +directory = Path(__file__).resolve().parent + + +def test__tracer_for_instance(analysis_imaging_7x7): + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, + light=al.lp.SersicSph(intensity=2.0), + mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ), + source=al.Galaxy(redshift=1.0), + ) + + af.Collection( + extra_galaxy=al.Galaxy( + redshift=0.5, + light=al.lp.SersicSph(intensity=0.1), + mass=al.mp.IsothermalSph(einstein_radius=0.2), + ) + ), + ) + + instance = model.instance_from_unit_vector([]) + tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) + + assert tracer.galaxies[0].redshift == 0.5 + assert tracer.galaxies[0].light.intensity == 2.0 + assert tracer.galaxies[0].mass.centre == pytest.approx((0.0, 0.0), 1.0e-4) + assert tracer.galaxies[0].mass.einstein_radius == 1.0 + assert tracer.galaxies[2].redshift == 0.5 + assert tracer.galaxies[2].light.intensity == 0.1 + assert tracer.galaxies[2].mass.einstein_radius == 0.2 + + +def test__tracer_for_instance__subhalo_redshift_rescale_used(analysis_imaging_7x7): + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, + mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ), + subhalo=al.Galaxy(redshift=0.25, mass=al.mp.NFWSph(centre=(0.1, 0.2))), + source=al.Galaxy(redshift=1.0), + ) + ) + + instance = model.instance_from_unit_vector([]) + tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) + + assert tracer.galaxies[1].mass.centre == pytest.approx((0.1, 0.2), 1.0e-4) + + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, + mass=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ), + subhalo=al.Galaxy(redshift=0.75, mass=al.mp.NFWSph(centre=(0.1, 0.2))), + source=al.Galaxy(redshift=1.0), + ) + ) + + instance = model.instance_from_unit_vector([]) + tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance) + + assert tracer.galaxies[1].mass.centre == pytest.approx((-0.19959, -0.39919), 1.0e-4) + + +def test__use_border_relocator__determines_if_border_pixel_relocation_is_used( + masked_imaging_7x7, +): + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=1.0), + ) + + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, mass=al.mp.IsothermalSph(einstein_radius=100.0) + ), + source=al.Galaxy(redshift=1.0, pixelization=pixelization), + ) + ) + + masked_imaging_7x7.grids.lp.over_sampled[4] = np.array([300.0, 0.0]) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, + settings=al.Settings(use_border_relocator=False), + use_jax=False, + ) + + instance = model.instance_from_unit_vector([]) + fit = analysis.fit_from(instance=instance) + + grid = fit.inversion.linear_obj_list[0].source_plane_data_grid.over_sampled + + assert grid[2] == pytest.approx([-82.99114877, 52.81254922], 1.0e-4) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, + settings=al.Settings(use_border_relocator=True), + use_jax=False, + ) + + instance = model.instance_from_unit_vector([]) + fit = analysis.fit_from(instance=instance) + + grid = fit.inversion.linear_obj_list[0].source_plane_data_grid.over_sampled + + assert grid[2] == pytest.approx([-82.991148773, 52.81254921], 1.0e-4) + + +def test__modify_before_fit__inversion_no_positions_likelihood__raises_exception( + masked_imaging_7x7, +): + lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph()) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(), regularization=al.reg.Constant() + ) + + source = al.Galaxy(redshift=1.0, pixelization=pixelization) + + model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) + + with pytest.raises(exc.AnalysisException): + analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) + + positions_likelihood = al.PositionsLH( + positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 + ) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, + positions_likelihood_list=[positions_likelihood], + use_jax=False, + ) + analysis.modify_before_fit(paths=af.DirectoryPaths(), model=model) + + +def test__save_results__tracer_output_to_json(analysis_imaging_7x7): + lens = al.Galaxy(redshift=0.5) + source = al.Galaxy(redshift=1.0) + + model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) + + tracer = al.Tracer(galaxies=[lens, source]) + + paths = af.DirectoryPaths() + + analysis_imaging_7x7.save_results( + paths=paths, + result=al.m.MockResult(max_log_likelihood_tracer=tracer, model=model), + ) + + tracer = from_json(file_path=paths._files_path / "tracer.json") + + assert tracer.galaxies[0].redshift == 0.5 + assert tracer.galaxies[1].redshift == 1.0 + + os.remove(paths._files_path / "tracer.json") diff --git a/test_autolens/analysis/test_positions.py b/test_autolens/analysis/test_positions.py index 4864c3c4d..7f5d7efaf 100644 --- a/test_autolens/analysis/test_positions.py +++ b/test_autolens/analysis/test_positions.py @@ -1,81 +1,81 @@ -import os -from pathlib import Path -import pytest - -from autonerves.dictable import output_to_json, from_json, from_dict -from autofit.tools.util import open_ - -import autolens as al -from autolens import exc - - -def test__check_positions_on_instantiation(): - al.PositionsLH( - positions=al.Grid2DIrregular([(1.0, 2.0), (3.0, 4.0)]), threshold=0.1 - ) - - # Positions input with threshold but positions are length 1. - - with pytest.raises(exc.PositionsException): - al.PositionsLH(positions=al.Grid2DIrregular([(1.0, 2.0)]), threshold=0.1) - - -def test__output_positions_info(): - output_path = Path(__file__).resolve().parent / "files" - - positions_likelihood = al.PositionsLH( - positions=al.Grid2DIrregular([(1.0, 2.0), (3.0, 4.0)]), threshold=0.1 - ) - - tracer = al.m.MockTracer( - traced_grid_2d_list_from=al.Grid2DIrregular(values=[[(0.5, 1.5), (2.5, 3.5)]]) - ) - - positions_likelihood.output_positions_info(output_path=output_path, tracer=tracer) - - positions_file = output_path / "positions.info" - - with open_(positions_file, "r") as f: - output = f.readlines() - - assert "Plane Index: -1" in output[0] - assert "Positions" in output[1] - - os.remove(positions_file) - - -@pytest.fixture(name="settings_dict") -def make_settings_dict(): - return { - "type": "instance", - "class_path": "autolens.analysis.positions.PositionsLH", - "arguments": { - "positions": { - "type": "ndarray", - "array": [[1.0, 2.0], [3.0, 4.0]], - "dtype": "float64", - }, - "threshold": 0.1, - "log_likelihood_penalty_factor": 100000000.0, - }, - } - - -def test_settings_from_dict(settings_dict): - assert isinstance(from_dict(settings_dict), al.PositionsLH) - - -def test_file(): - filename = "/tmp/temp.json" - - output_to_json( - al.PositionsLH( - positions=al.Grid2DIrregular([(1.0, 2.0), (3.0, 4.0)]), threshold=0.1 - ), - filename, - ) - - try: - assert isinstance(from_json(filename), al.PositionsLH) - finally: - os.remove(filename) +import os +from pathlib import Path +import pytest + +from autonerves.dictable import output_to_json, from_json, from_dict +from autofit.tools.util import open_ + +import autolens as al +from autolens import exc + + +def test__check_positions_on_instantiation(): + al.PositionsLH( + positions=al.Grid2DIrregular([(1.0, 2.0), (3.0, 4.0)]), threshold=0.1 + ) + + # Positions input with threshold but positions are length 1. + + with pytest.raises(exc.PositionsException): + al.PositionsLH(positions=al.Grid2DIrregular([(1.0, 2.0)]), threshold=0.1) + + +def test__output_positions_info(): + output_path = Path(__file__).resolve().parent / "files" + + positions_likelihood = al.PositionsLH( + positions=al.Grid2DIrregular([(1.0, 2.0), (3.0, 4.0)]), threshold=0.1 + ) + + tracer = al.m.MockTracer( + traced_grid_2d_list_from=al.Grid2DIrregular(values=[[(0.5, 1.5), (2.5, 3.5)]]) + ) + + positions_likelihood.output_positions_info(output_path=output_path, tracer=tracer) + + positions_file = output_path / "positions.info" + + with open_(positions_file, "r") as f: + output = f.readlines() + + assert "Plane Index: -1" in output[0] + assert "Positions" in output[1] + + os.remove(positions_file) + + +@pytest.fixture(name="settings_dict") +def make_settings_dict(): + return { + "type": "instance", + "class_path": "autolens.analysis.positions.PositionsLH", + "arguments": { + "positions": { + "type": "ndarray", + "array": [[1.0, 2.0], [3.0, 4.0]], + "dtype": "float64", + }, + "threshold": 0.1, + "log_likelihood_penalty_factor": 100000000.0, + }, + } + + +def test_settings_from_dict(settings_dict): + assert isinstance(from_dict(settings_dict), al.PositionsLH) + + +def test_file(): + filename = "/tmp/temp.json" + + output_to_json( + al.PositionsLH( + positions=al.Grid2DIrregular([(1.0, 2.0), (3.0, 4.0)]), threshold=0.1 + ), + filename, + ) + + try: + assert isinstance(from_json(filename), al.PositionsLH) + finally: + os.remove(filename) diff --git a/test_autolens/config/general.yaml b/test_autolens/config/general.yaml index 1a7a58c5d..ed6e24322 100644 --- a/test_autolens/config/general.yaml +++ b/test_autolens/config/general.yaml @@ -1,37 +1,37 @@ -analysis: - n_cores: 1 -psf: - use_fft_default: false # If True, PSFs are convolved using FFTs by default, which is faster and uses less memory in all cases except for very small PSFs, False uses direct convolution. -hpc: - hpc_mode: false - iterations_per_full_update: 5000 -adapt: - adapt_minimum_percent: 0.01 - adapt_noise_limit: 100000000.0 -inversion: - check_reconstruction: false # If True, the inversion's reconstruction is checked to ensure the solution of a meshs's mapper is not an invalid solution where the values are all the same. - use_positive_only_solver: false # If True, inversion's use a positive-only linear algebra solver by default, which is slower but prevents unphysical negative values in the reconstructed solutuion. - no_regularization_add_to_curvature_diag_value: 1.0e-8 # The default value added to the curvature matrix's diagonal when regularization is not applied to a linear object, which prevents inversion's failing due to the matrix being singular. - positive_only_uses_p_initial: false # If True, the positive-only solver of an inversion's uses an initial guess of the reconstructed data's values as which values should be positive, speeding up the solver. - use_border_relocator: false # If True, by default a pixelization's border is used to relocate all pixels outside its border to the border. -numba: - cache: true - nopython: true - parallel: false - use_numba: true -output: - force_pickle_overwrite: false - info_whitespace_length: 80 - log_file: output.log - log_level: INFO - log_to_file: false - model_results_decimal_places: 3 - remove_files: false - samples_to_csv: false -profiling: - perform: true - repeats: 1 -test: - check_likelihood_function: false # if True, when a search is resumed the likelihood of a previous sample is recalculated to ensure it is consistent with the previous run. - exception_override: false - disable_positions_lh_inversion_check: false +analysis: + n_cores: 1 +psf: + use_fft_default: false # If True, PSFs are convolved using FFTs by default, which is faster and uses less memory in all cases except for very small PSFs, False uses direct convolution. +hpc: + hpc_mode: false + iterations_per_full_update: 5000 +adapt: + adapt_minimum_percent: 0.01 + adapt_noise_limit: 100000000.0 +inversion: + check_reconstruction: false # If True, the inversion's reconstruction is checked to ensure the solution of a meshs's mapper is not an invalid solution where the values are all the same. + use_positive_only_solver: false # If True, inversion's use a positive-only linear algebra solver by default, which is slower but prevents unphysical negative values in the reconstructed solutuion. + no_regularization_add_to_curvature_diag_value: 1.0e-8 # The default value added to the curvature matrix's diagonal when regularization is not applied to a linear object, which prevents inversion's failing due to the matrix being singular. + positive_only_uses_p_initial: false # If True, the positive-only solver of an inversion's uses an initial guess of the reconstructed data's values as which values should be positive, speeding up the solver. + use_border_relocator: false # If True, by default a pixelization's border is used to relocate all pixels outside its border to the border. +numba: + cache: true + nopython: true + parallel: false + use_numba: true +output: + force_pickle_overwrite: false + info_whitespace_length: 80 + log_file: output.log + log_level: INFO + log_to_file: false + model_results_decimal_places: 3 + remove_files: false + samples_to_csv: false +profiling: + perform: true + repeats: 1 +test: + check_likelihood_function: false # if True, when a search is resumed the likelihood of a previous sample is recalculated to ensure it is consistent with the previous run. + exception_override: false + disable_positions_lh_inversion_check: false diff --git a/test_autolens/config/non_linear.yaml b/test_autolens/config/non_linear.yaml index 31784457d..32fca6435 100644 --- a/test_autolens/config/non_linear.yaml +++ b/test_autolens/config/non_linear.yaml @@ -1,75 +1,75 @@ -mock: - MockOptimizer: - initialize: - method: prior - printing: - silence: false - - MockSearch: - initialize: - method: prior - printing: - silence: false - search: {} - -nest: - DynestyDynamic: - general: - acceptance_ratio_threshold: 0.1 - bootstrap: null - bound: multi - enlarge: null - first_update: null - fmove: 0.9 - max_move: 100 - sample: auto - sampling_efficiency: 0.5 - slices: 5 - terminate_at_acceptance_ratio: false - update_interval: null - walks: 25 - initialize: - method: prior - parallel: - force_x1_cpu: false - number_of_cores: 1 - printing: - silence: false - - DynestyStatic: - parallel: - number_of_cores: 1 - initialize: - method: prior - inversion: - acceptance_ratio_threshold: 0.05 - const_efficiency_mode: true - evidence_tolerance: 100.0 - multimodal: false - n_live_points: 50 - sampling_efficiency: 0.3 - terminate_at_acceptance_ratio: false - printing: - silence: false - search: - const_efficiency_mode: false - evidence_tolerance: 0.5 - importance_nested_sampling: false - max_iter: 0 - max_modes: 100 - mode_tolerance: -1.0e+90 - multimodal: false - n_live_points: 50 - sampling_efficiency: 0.5 - settings: - context: 0 - init_MPI: false - log_zero: -1.0e+100 - n_iter_before_update: 5 - null_log_evidence: -1.0e+90 - resume: true - seed: -1.0 - stagger_resampling_likelihood: true - verbose: false - write_output: true - +mock: + MockOptimizer: + initialize: + method: prior + printing: + silence: false + + MockSearch: + initialize: + method: prior + printing: + silence: false + search: {} + +nest: + DynestyDynamic: + general: + acceptance_ratio_threshold: 0.1 + bootstrap: null + bound: multi + enlarge: null + first_update: null + fmove: 0.9 + max_move: 100 + sample: auto + sampling_efficiency: 0.5 + slices: 5 + terminate_at_acceptance_ratio: false + update_interval: null + walks: 25 + initialize: + method: prior + parallel: + force_x1_cpu: false + number_of_cores: 1 + printing: + silence: false + + DynestyStatic: + parallel: + number_of_cores: 1 + initialize: + method: prior + inversion: + acceptance_ratio_threshold: 0.05 + const_efficiency_mode: true + evidence_tolerance: 100.0 + multimodal: false + n_live_points: 50 + sampling_efficiency: 0.3 + terminate_at_acceptance_ratio: false + printing: + silence: false + search: + const_efficiency_mode: false + evidence_tolerance: 0.5 + importance_nested_sampling: false + max_iter: 0 + max_modes: 100 + mode_tolerance: -1.0e+90 + multimodal: false + n_live_points: 50 + sampling_efficiency: 0.5 + settings: + context: 0 + init_MPI: false + log_zero: -1.0e+100 + n_iter_before_update: 5 + null_log_evidence: -1.0e+90 + resume: true + seed: -1.0 + stagger_resampling_likelihood: true + verbose: false + write_output: true + diff --git a/test_autolens/config/notation.yaml b/test_autolens/config/notation.yaml index 92a53b924..9f35cca40 100644 --- a/test_autolens/config/notation.yaml +++ b/test_autolens/config/notation.yaml @@ -1,83 +1,83 @@ -label: - label: - alpha: \alpha - angle_binary: \theta - beta: \beta - break_radius: \theta_{\rm B} - centre_0: y - centre_1: x - coefficient: \lambda - contribution_factor: \omega_{\rm 0} - core_radius: C_{\rm r} - core_radius_0: C_{rm r0} - core_radius_1: C_{\rm r1} - effective_radius: R_{\rm eff} - einstein_radius: \theta_{\rm Ein} - ell_comps_0: \epsilon_{\rm 1} - ell_comps_1: \epsilon_{\rm 2} - multipole_comps_0: M_{\rm 1} - multipole_comps_1: M_{\rm 2} - flux: F - gamma: \gamma - gamma_1: \gamma - gamma_2: \gamma - inner_coefficient: \lambda_{\rm 1} - inner_slope: t_{\rm 1} - intensity: I_{\rm b} - kappa: \kappa - kappa_s: \kappa_{\rm s} - log10m_vir: log_{\rm 10}(m_{vir}) - m: m - mass: M - mass_at_200: M_{\rm 200} - mass_ratio: M_{\rm ratio} - mass_to_light_gradient: \Gamma - mass_to_light_ratio: \Psi - mass_to_light_ratio_base: \Psi_{\rm base} - mass_to_light_radius: R_{\rm ref} - noise_factor: \omega_{\rm 1} - noise_power: \omega{\rm 2} - noise_scale: \sigma_{\rm 1} - normalization_scale: n - outer_coefficient: \lambda_{\rm 2} - outer_slope: t_{\rm 2} - overdens: \Delta_{\rm vir} - pixels: N_{\rm pix} - radius_break: R_{\rm b} - redshift: z - redshift_object: z_{\rm obj} - redshift_source: z_{\rm src} - scale_radius: R_{\rm s} - scatter: \sigma - separation: s - sersic_index: n - shape_0: y_{\rm pix} - shape_1: x_{\rm pix} - sigma: \sigma - signal_scale: V - sky_scale: \sigma_{\rm 0} - slope: \gamma - truncation_radius: R_{\rm t} - weight_floor: W_{\rm f} - weight_power: W_{\rm p} - superscript: - ExternalShear: ext - Pixelization: pix - Point: point - Redshift: '' - Regularization: reg -label_format: - format: - angular_diameter_distance_to_earth: '{:.2f}' - concentration: '{:.2f}' - einstein_mass: '{:.4e}' - einstein_radius: '{:.2f}' - kpc_per_arcsec: '{:.2f}' - luminosity: '{:.4e}' - m: '{:.1f}' - mass: '{:.4e}' - mass_at_truncation_radius: '{:.4e}' - radius: '{:.2f}' - redshift: '{:.2f}' - rho: '{:.2f}' - sersic_luminosity: '{:.4e}' +label: + label: + alpha: \alpha + angle_binary: \theta + beta: \beta + break_radius: \theta_{\rm B} + centre_0: y + centre_1: x + coefficient: \lambda + contribution_factor: \omega_{\rm 0} + core_radius: C_{\rm r} + core_radius_0: C_{rm r0} + core_radius_1: C_{\rm r1} + effective_radius: R_{\rm eff} + einstein_radius: \theta_{\rm Ein} + ell_comps_0: \epsilon_{\rm 1} + ell_comps_1: \epsilon_{\rm 2} + multipole_comps_0: M_{\rm 1} + multipole_comps_1: M_{\rm 2} + flux: F + gamma: \gamma + gamma_1: \gamma + gamma_2: \gamma + inner_coefficient: \lambda_{\rm 1} + inner_slope: t_{\rm 1} + intensity: I_{\rm b} + kappa: \kappa + kappa_s: \kappa_{\rm s} + log10m_vir: log_{\rm 10}(m_{vir}) + m: m + mass: M + mass_at_200: M_{\rm 200} + mass_ratio: M_{\rm ratio} + mass_to_light_gradient: \Gamma + mass_to_light_ratio: \Psi + mass_to_light_ratio_base: \Psi_{\rm base} + mass_to_light_radius: R_{\rm ref} + noise_factor: \omega_{\rm 1} + noise_power: \omega{\rm 2} + noise_scale: \sigma_{\rm 1} + normalization_scale: n + outer_coefficient: \lambda_{\rm 2} + outer_slope: t_{\rm 2} + overdens: \Delta_{\rm vir} + pixels: N_{\rm pix} + radius_break: R_{\rm b} + redshift: z + redshift_object: z_{\rm obj} + redshift_source: z_{\rm src} + scale_radius: R_{\rm s} + scatter: \sigma + separation: s + sersic_index: n + shape_0: y_{\rm pix} + shape_1: x_{\rm pix} + sigma: \sigma + signal_scale: V + sky_scale: \sigma_{\rm 0} + slope: \gamma + truncation_radius: R_{\rm t} + weight_floor: W_{\rm f} + weight_power: W_{\rm p} + superscript: + ExternalShear: ext + Pixelization: pix + Point: point + Redshift: '' + Regularization: reg +label_format: + format: + angular_diameter_distance_to_earth: '{:.2f}' + concentration: '{:.2f}' + einstein_mass: '{:.4e}' + einstein_radius: '{:.2f}' + kpc_per_arcsec: '{:.2f}' + luminosity: '{:.4e}' + m: '{:.1f}' + mass: '{:.4e}' + mass_at_truncation_radius: '{:.4e}' + radius: '{:.2f}' + redshift: '{:.2f}' + rho: '{:.2f}' + sersic_luminosity: '{:.4e}' diff --git a/test_autolens/config/output.yaml b/test_autolens/config/output.yaml index 4a44b384b..5cb8812dc 100644 --- a/test_autolens/config/output.yaml +++ b/test_autolens/config/output.yaml @@ -1,63 +1,63 @@ -# Determines whether files saved by the search are output to the hard-disk. This is true both when saving to the -# directory structure and when saving to database. - -# Files can be listed name: bool where the name is the name of the file without a suffix (e.g. model not model.json) -# and bool is true or false. - -# If a given file is not listed then the default value is used. - -default: true # If true then files which are not explicitly listed here are output anyway. If false then they are not. - -### Samples ### - -# The `samples.csv`file contains every sampled value of every free parameter with its log likelihood and weight. - -# This file is often large, therefore disabling it can significantly reduce hard-disk space use. - -# `samples.csv` is used to perform marginalization, infer model parameter errors and do other analysis of the search -# chains. Even if output of `samples.csv` is disabled, these tasks are still performed by the fit and output to -# the `samples_summary.json` file. However, without a `samples.csv` file these types of tasks cannot be performed -# after the fit is complete, for example via the database. - -samples: true - -# The `samples.csv` file contains every accepted sampled value of every free parameter with its log likelihood and -# weight. For certain searches, the majority of samples have a very low weight, which has no numerical impact on the -# results of the model-fit. However, these samples are still output to the `samples.csv` file, taking up hard-disk space -# and slowing down analysis of the samples (e.g. via the database). - -# The `samples_weight_threshold` below specifies the threshold value of the weight such that samples with a weight -# below this value are not output to the `samples.csv` file. This can be used to reduce the size of the `samples.csv` -# file and speed up analysis of the samples. - -# Note that for many searches (e.g. MCMC) all samples have equal weight, and thus this threshold has no impact and -# there is no simple way to save hard-disk space. However, for nested sampling, the majority of samples have a very -# low weight and this threshold can be used to save hard-disk space. - -# Set value to empty (e.g. delete 1.0e-10 below) to disable this feature. - -samples_weight_threshold: 1.0e-10 - -### Search Internal ### - -# The search internal folder which contains a saved state of the non-linear search, as a .pickle or .dill file. - -# If the entry below is false, the folder is still output during the model-fit, as it is required to resume the fit -# from where it left off. Therefore, settings `false` below does not impact model-fitting checkpointing and resumption. -# Instead, the search internal folder is deleted once the fit is completed. - -# The search internal folder file is often large, therefore deleting it after a fit is complete can significantly -# reduce hard-disk space use. - -# The search internal representation (e.g. what you can load from the output .pickle file) may have additional -# quantities specific to the non-linear search that you are interested in inspecting. Deleting the folder means this -# information is list. - -search_internal: false - -# Other Files: - -covariance: false # `covariance.csv`: The [free parameters x free parameters] covariance matrix. -data: true # `data.json`: The value of every data point in the data. -noise_map: true # `noise_map.json`: The value of every RMS noise map value. - +# Determines whether files saved by the search are output to the hard-disk. This is true both when saving to the +# directory structure and when saving to database. + +# Files can be listed name: bool where the name is the name of the file without a suffix (e.g. model not model.json) +# and bool is true or false. + +# If a given file is not listed then the default value is used. + +default: true # If true then files which are not explicitly listed here are output anyway. If false then they are not. + +### Samples ### + +# The `samples.csv`file contains every sampled value of every free parameter with its log likelihood and weight. + +# This file is often large, therefore disabling it can significantly reduce hard-disk space use. + +# `samples.csv` is used to perform marginalization, infer model parameter errors and do other analysis of the search +# chains. Even if output of `samples.csv` is disabled, these tasks are still performed by the fit and output to +# the `samples_summary.json` file. However, without a `samples.csv` file these types of tasks cannot be performed +# after the fit is complete, for example via the database. + +samples: true + +# The `samples.csv` file contains every accepted sampled value of every free parameter with its log likelihood and +# weight. For certain searches, the majority of samples have a very low weight, which has no numerical impact on the +# results of the model-fit. However, these samples are still output to the `samples.csv` file, taking up hard-disk space +# and slowing down analysis of the samples (e.g. via the database). + +# The `samples_weight_threshold` below specifies the threshold value of the weight such that samples with a weight +# below this value are not output to the `samples.csv` file. This can be used to reduce the size of the `samples.csv` +# file and speed up analysis of the samples. + +# Note that for many searches (e.g. MCMC) all samples have equal weight, and thus this threshold has no impact and +# there is no simple way to save hard-disk space. However, for nested sampling, the majority of samples have a very +# low weight and this threshold can be used to save hard-disk space. + +# Set value to empty (e.g. delete 1.0e-10 below) to disable this feature. + +samples_weight_threshold: 1.0e-10 + +### Search Internal ### + +# The search internal folder which contains a saved state of the non-linear search, as a .pickle or .dill file. + +# If the entry below is false, the folder is still output during the model-fit, as it is required to resume the fit +# from where it left off. Therefore, settings `false` below does not impact model-fitting checkpointing and resumption. +# Instead, the search internal folder is deleted once the fit is completed. + +# The search internal folder file is often large, therefore deleting it after a fit is complete can significantly +# reduce hard-disk space use. + +# The search internal representation (e.g. what you can load from the output .pickle file) may have additional +# quantities specific to the non-linear search that you are interested in inspecting. Deleting the folder means this +# information is list. + +search_internal: false + +# Other Files: + +covariance: false # `covariance.csv`: The [free parameters x free parameters] covariance matrix. +data: true # `data.json`: The value of every data point in the data. +noise_map: true # `noise_map.json`: The value of every RMS noise map value. + diff --git a/test_autolens/config/priors/dark_mass_profiles.yaml b/test_autolens/config/priors/dark_mass_profiles.yaml index 61578a467..2135715ad 100644 --- a/test_autolens/config/priors/dark_mass_profiles.yaml +++ b/test_autolens/config/priors/dark_mass_profiles.yaml @@ -1,307 +1,307 @@ -NFW: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - kappa_s: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Relative - value: 0.2 - scale_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 0.2 -NFWSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - kappa_s: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Relative - value: 0.2 - scale_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 0.2 -NFWTruncatedSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - kappa_s: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Relative - value: 0.2 - scale_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 0.2 - truncation_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 0.2 -SphNFWTruncatedMCR: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - mass_at_200: - limits: - lower: 0.0 - upper: inf - lower_limit: 100000000.0 - type: LogUniform - upper_limit: 1000000000000000.0 - width_modifier: - type: Relative - value: 0.5 -gNFW: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - inner_slope: - limits: - lower: -1.0 - upper: 3.0 - lower_limit: 0.0 - type: Uniform - upper_limit: 2.0 - width_modifier: - type: Absolute - value: 0.3 - kappa_s: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Relative - value: 0.2 - scale_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 0.2 +NFW: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + kappa_s: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.2 + scale_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 0.2 +NFWSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + kappa_s: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.2 + scale_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 0.2 +NFWTruncatedSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + kappa_s: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.2 + scale_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 0.2 + truncation_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 0.2 +SphNFWTruncatedMCR: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + mass_at_200: + limits: + lower: 0.0 + upper: inf + lower_limit: 100000000.0 + type: LogUniform + upper_limit: 1000000000000000.0 + width_modifier: + type: Relative + value: 0.5 +gNFW: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + inner_slope: + limits: + lower: -1.0 + upper: 3.0 + lower_limit: 0.0 + type: Uniform + upper_limit: 2.0 + width_modifier: + type: Absolute + value: 0.3 + kappa_s: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.2 + scale_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 0.2 gNFWSph: centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - inner_slope: - limits: - lower: -1.0 - upper: 3.0 - lower_limit: 0.0 - type: Uniform - upper_limit: 2.0 - width_modifier: - type: Absolute - value: 0.3 - kappa_s: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Relative - value: 0.2 + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + inner_slope: + limits: + lower: -1.0 + upper: 3.0 + lower_limit: 0.0 + type: Uniform + upper_limit: 2.0 + width_modifier: + type: Absolute + value: 0.3 + kappa_s: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.2 scale_radius: limits: lower: 0.0 diff --git a/test_autolens/config/priors/galaxy.yaml b/test_autolens/config/priors/galaxy.yaml index be4cd3a1a..6461cb4c8 100644 --- a/test_autolens/config/priors/galaxy.yaml +++ b/test_autolens/config/priors/galaxy.yaml @@ -1,17 +1,17 @@ -Galaxy: - redshift: - lower_limit: 0.0 - type: Uniform - upper_limit: 3.0 - -Redshift: - redshift: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 3.0 - width_modifier: - type: Absolute - value: 1.0 +Galaxy: + redshift: + lower_limit: 0.0 + type: Uniform + upper_limit: 3.0 + +Redshift: + redshift: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 3.0 + width_modifier: + type: Absolute + value: 1.0 diff --git a/test_autolens/config/priors/geometry_profiles.yaml b/test_autolens/config/priors/geometry_profiles.yaml index 2cc1e251b..b55820bc4 100644 --- a/test_autolens/config/priors/geometry_profiles.yaml +++ b/test_autolens/config/priors/geometry_profiles.yaml @@ -1,48 +1,48 @@ -AbstractSersic: - angle: - lower_limit: 0.0 - type: Uniform - upper_limit: 180.0 - axis_ratio: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - centre_0: - lower_limit: -1.0 - type: Uniform - upper_limit: 1.0 - centre_1: - lower_limit: -1.0 - type: Uniform - upper_limit: 1.0 - effective_radius: - lower_limit: 0.0 - type: Uniform - upper_limit: 3.0 - intensity: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - sersic_index: - lower_limit: 0.6 - type: Uniform - upper_limit: 8.0 -EllProfile: - angle: - lower_limit: 0.0 - type: Uniform - upper_limit: 2.0 - axis_ratio: - lower_limit: 0.0 - type: Uniform - upper_limit: 2.0 -GeometryProfile: - centre_0: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - centre_1: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 -SphlProfile: {} +AbstractSersic: + angle: + lower_limit: 0.0 + type: Uniform + upper_limit: 180.0 + axis_ratio: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + centre_0: + lower_limit: -1.0 + type: Uniform + upper_limit: 1.0 + centre_1: + lower_limit: -1.0 + type: Uniform + upper_limit: 1.0 + effective_radius: + lower_limit: 0.0 + type: Uniform + upper_limit: 3.0 + intensity: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + sersic_index: + lower_limit: 0.6 + type: Uniform + upper_limit: 8.0 +EllProfile: + angle: + lower_limit: 0.0 + type: Uniform + upper_limit: 2.0 + axis_ratio: + lower_limit: 0.0 + type: Uniform + upper_limit: 2.0 +GeometryProfile: + centre_0: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + centre_1: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 +SphlProfile: {} diff --git a/test_autolens/config/priors/light_profiles.yaml b/test_autolens/config/priors/light_profiles.yaml index f1c457dfb..ecb94c26d 100644 --- a/test_autolens/config/priors/light_profiles.yaml +++ b/test_autolens/config/priors/light_profiles.yaml @@ -1,250 +1,250 @@ -EllLightProfile: - centre_0: - limits: - lower: -inf - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 0.5 - width_modifier: - type: Absolute - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - intensity: - mean: 0.0 - sigma: 0.5 - type: Gaussian - width_modifier: - type: Relative - value: 0.5 -Exponential: - centre_0: - limits: - lower: -inf - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 0.5 - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 2.0 - width_modifier: - type: Absolute - value: 2.0 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - intensity: - mean: 0.0 - sigma: 0.5 - type: Gaussian - width_modifier: - type: Relative - value: 0.5 -Gaussian: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000000.0 - width_modifier: - type: Relative - value: 0.5 - sigma: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 25.0 - width_modifier: - type: Relative - value: 0.5 -Sersic: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.2 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000000.0 - width_modifier: - type: Relative - value: 0.5 - sersic_index: - limits: - lower: 0.5 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 8.0 - width_modifier: - type: Absolute - value: 1.5 +EllLightProfile: + centre_0: + limits: + lower: -inf + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 0.5 + width_modifier: + type: Absolute + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + intensity: + mean: 0.0 + sigma: 0.5 + type: Gaussian + width_modifier: + type: Relative + value: 0.5 +Exponential: + centre_0: + limits: + lower: -inf + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 0.5 + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 2.0 + width_modifier: + type: Absolute + value: 2.0 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + intensity: + mean: 0.0 + sigma: 0.5 + type: Gaussian + width_modifier: + type: Relative + value: 0.5 +Gaussian: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000000.0 + width_modifier: + type: Relative + value: 0.5 + sigma: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 25.0 + width_modifier: + type: Relative + value: 0.5 +Sersic: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.2 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000000.0 + width_modifier: + type: Relative + value: 0.5 + sersic_index: + limits: + lower: 0.5 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 8.0 + width_modifier: + type: Absolute + value: 1.5 diff --git a/test_autolens/config/priors/mass_sheets.yaml b/test_autolens/config/priors/mass_sheets.yaml index 5d01c5cb9..2dd0e2268 100644 --- a/test_autolens/config/priors/mass_sheets.yaml +++ b/test_autolens/config/priors/mass_sheets.yaml @@ -1,21 +1,21 @@ -ExternalShear: - gamma_1: - limits: - lower: -inf - upper: inf - lower_limit: -0.2 - type: Uniform - upper_limit: 0.2 - width_modifier: - type: Absolute - value: 0.05 - gamma_2: - limits: - lower: -inf - upper: inf - lower_limit: -0.2 - type: Uniform - upper_limit: 0.2 - width_modifier: - type: Absolute - value: 0.05 +ExternalShear: + gamma_1: + limits: + lower: -inf + upper: inf + lower_limit: -0.2 + type: Uniform + upper_limit: 0.2 + width_modifier: + type: Absolute + value: 0.05 + gamma_2: + limits: + lower: -inf + upper: inf + lower_limit: -0.2 + type: Uniform + upper_limit: 0.2 + width_modifier: + type: Absolute + value: 0.05 diff --git a/test_autolens/config/priors/pixelizations.yaml b/test_autolens/config/priors/pixelizations.yaml index 5473b34f9..236268359 100644 --- a/test_autolens/config/priors/pixelizations.yaml +++ b/test_autolens/config/priors/pixelizations.yaml @@ -1,125 +1,125 @@ -delaunay.DelaunayBrightnessImage: - pixels: - limits: - lower: 50.0 - upper: inf - lower_limit: 50.0 - type: Uniform - upper_limit: 2500.0 - width_modifier: - type: Absolute - value: 100.0 - weight_floor: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.1 - weight_power: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 20.0 - width_modifier: - type: Absolute - value: 5.0 -delaunay.DelaunayMagnification: - shape_0: - limits: - lower: 3.0 - upper: inf - lower_limit: 20.0 - type: Uniform - upper_limit: 45.0 - width_modifier: - type: Absolute - value: 8.0 - shape_1: - limits: - lower: 3.0 - upper: inf - lower_limit: 20.0 - type: Uniform - upper_limit: 45.0 - width_modifier: - type: Absolute - value: 8.0 -rectangular.RectangularAdaptDensity: - shape_0: - limits: - lower: 3.0 - upper: inf - lower_limit: 20.0 - type: Uniform - upper_limit: 45.0 - width_modifier: - type: Absolute - value: 8.0 - shape_1: - limits: - lower: 3.0 - upper: inf - lower_limit: 20.0 - type: Uniform - upper_limit: 45.0 - width_modifier: - type: Absolute - value: 8.0 -voronoi.VoronoiBrightnessImage: - pixels: - limits: - lower: 0.0 - upper: inf - lower_limit: 50.0 - type: Uniform - upper_limit: 1500.0 - width_modifier: - type: Absolute - value: 400.0 - weight_floor: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.1 - weight_power: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 20.0 - width_modifier: - type: Absolute - value: 5.0 -voronoi.VoronoiMagnification: - shape_0: - limits: - lower: 3.0 - upper: inf - lower_limit: 20.0 - type: Uniform - upper_limit: 45.0 - width_modifier: - type: Absolute - value: 8.0 - shape_1: - limits: - lower: 3.0 - upper: inf - lower_limit: 20.0 - type: Uniform - upper_limit: 45.0 - width_modifier: - type: Absolute - value: 8.0 +delaunay.DelaunayBrightnessImage: + pixels: + limits: + lower: 50.0 + upper: inf + lower_limit: 50.0 + type: Uniform + upper_limit: 2500.0 + width_modifier: + type: Absolute + value: 100.0 + weight_floor: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.1 + weight_power: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 20.0 + width_modifier: + type: Absolute + value: 5.0 +delaunay.DelaunayMagnification: + shape_0: + limits: + lower: 3.0 + upper: inf + lower_limit: 20.0 + type: Uniform + upper_limit: 45.0 + width_modifier: + type: Absolute + value: 8.0 + shape_1: + limits: + lower: 3.0 + upper: inf + lower_limit: 20.0 + type: Uniform + upper_limit: 45.0 + width_modifier: + type: Absolute + value: 8.0 +rectangular.RectangularAdaptDensity: + shape_0: + limits: + lower: 3.0 + upper: inf + lower_limit: 20.0 + type: Uniform + upper_limit: 45.0 + width_modifier: + type: Absolute + value: 8.0 + shape_1: + limits: + lower: 3.0 + upper: inf + lower_limit: 20.0 + type: Uniform + upper_limit: 45.0 + width_modifier: + type: Absolute + value: 8.0 +voronoi.VoronoiBrightnessImage: + pixels: + limits: + lower: 0.0 + upper: inf + lower_limit: 50.0 + type: Uniform + upper_limit: 1500.0 + width_modifier: + type: Absolute + value: 400.0 + weight_floor: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.1 + weight_power: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 20.0 + width_modifier: + type: Absolute + value: 5.0 +voronoi.VoronoiMagnification: + shape_0: + limits: + lower: 3.0 + upper: inf + lower_limit: 20.0 + type: Uniform + upper_limit: 45.0 + width_modifier: + type: Absolute + value: 8.0 + shape_1: + limits: + lower: 3.0 + upper: inf + lower_limit: 20.0 + type: Uniform + upper_limit: 45.0 + width_modifier: + type: Absolute + value: 8.0 diff --git a/test_autolens/config/priors/regularization.yaml b/test_autolens/config/priors/regularization.yaml index c5288d588..732372ece 100644 --- a/test_autolens/config/priors/regularization.yaml +++ b/test_autolens/config/priors/regularization.yaml @@ -1,39 +1,39 @@ -adaptive_brightness.Adapt: - inner_coefficient: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - outer_coefficient: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 - signal_scale: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 -constant.Constant: - coefficient: - lower_limit: 0.0 - type: Uniform - upper_limit: 1.0 -constant_zeorth.ConstantZeroth: - coefficient_neighbor: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000000.0 - width_modifier: - type: Relative - value: 0.5 - coefficient_zeroth: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000000.0 - width_modifier: - type: Relative - value: 0.5 +adaptive_brightness.Adapt: + inner_coefficient: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + outer_coefficient: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 + signal_scale: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 +constant.Constant: + coefficient: + lower_limit: 0.0 + type: Uniform + upper_limit: 1.0 +constant_zeorth.ConstantZeroth: + coefficient_neighbor: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000000.0 + width_modifier: + type: Relative + value: 0.5 + coefficient_zeroth: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000000.0 + width_modifier: + type: Relative + value: 0.5 diff --git a/test_autolens/config/priors/stellar_mass_profiles.yaml b/test_autolens/config/priors/stellar_mass_profiles.yaml index 906006c5e..afdfb32ff 100644 --- a/test_autolens/config/priors/stellar_mass_profiles.yaml +++ b/test_autolens/config/priors/stellar_mass_profiles.yaml @@ -1,564 +1,564 @@ -DevVaucouleurs: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 -DevVaucouleursSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 -Exponential: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 -ExponentialSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 -Sersic: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 - sersic_index: - limits: - lower: 0.8 - upper: 5.0 - lower_limit: 0.8 - type: Uniform - upper_limit: 5.0 - width_modifier: - type: Absolute - value: 1.5 -SersicGradient: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_gradient: - limits: - lower: 0.0 - upper: inf - lower_limit: -1.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 - sersic_index: - limits: - lower: 0.8 - upper: 5.0 - lower_limit: 0.8 - type: Uniform - upper_limit: 5.0 - width_modifier: - type: Absolute - value: 1.5 -SersicGradientSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_gradient: - limits: - lower: 0.0 - upper: inf - lower_limit: -1.0 - type: Uniform - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 - sersic_index: - limits: - lower: 0.8 - upper: 5.0 - lower_limit: 0.8 - type: Uniform - upper_limit: 5.0 - width_modifier: - type: Absolute - value: 1.5 -SersicSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - effective_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 30.0 - width_modifier: - type: Relative - value: 1.0 - intensity: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 10.0 - width_modifier: - type: Relative - value: 0.5 - mass_to_light_ratio: - limits: - lower: 0.0 - upper: inf - lower_limit: 1.0e-06 - type: LogUniform - upper_limit: 1000.0 - width_modifier: - type: Relative - value: 0.3 - sersic_index: - limits: - lower: 0.8 - upper: 5.0 - lower_limit: 0.8 - type: Uniform - upper_limit: 5.0 - width_modifier: - type: Absolute - value: 1.5 +DevVaucouleurs: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 +DevVaucouleursSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 +Exponential: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 +ExponentialSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 +Sersic: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 + sersic_index: + limits: + lower: 0.8 + upper: 5.0 + lower_limit: 0.8 + type: Uniform + upper_limit: 5.0 + width_modifier: + type: Absolute + value: 1.5 +SersicGradient: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_gradient: + limits: + lower: 0.0 + upper: inf + lower_limit: -1.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 + sersic_index: + limits: + lower: 0.8 + upper: 5.0 + lower_limit: 0.8 + type: Uniform + upper_limit: 5.0 + width_modifier: + type: Absolute + value: 1.5 +SersicGradientSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_gradient: + limits: + lower: 0.0 + upper: inf + lower_limit: -1.0 + type: Uniform + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 + sersic_index: + limits: + lower: 0.8 + upper: 5.0 + lower_limit: 0.8 + type: Uniform + upper_limit: 5.0 + width_modifier: + type: Absolute + value: 1.5 +SersicSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + effective_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 30.0 + width_modifier: + type: Relative + value: 1.0 + intensity: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 10.0 + width_modifier: + type: Relative + value: 0.5 + mass_to_light_ratio: + limits: + lower: 0.0 + upper: inf + lower_limit: 1.0e-06 + type: LogUniform + upper_limit: 1000.0 + width_modifier: + type: Relative + value: 0.3 + sersic_index: + limits: + lower: 0.8 + upper: 5.0 + lower_limit: 0.8 + type: Uniform + upper_limit: 5.0 + width_modifier: + type: Absolute + value: 1.5 diff --git a/test_autolens/config/priors/total_mass_profiles.yaml b/test_autolens/config/priors/total_mass_profiles.yaml index 88e66045a..edd3f9535 100644 --- a/test_autolens/config/priors/total_mass_profiles.yaml +++ b/test_autolens/config/priors/total_mass_profiles.yaml @@ -1,455 +1,455 @@ -Isothermal: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 -IsothermalCore: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - core_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 0.2 - width_modifier: - type: Absolute - value: 0.1 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 -IsothermalCoreSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - core_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 0.2 - width_modifier: - type: Absolute - value: 0.1 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 -IsothermalSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 -PointMass: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 8.0 - width_modifier: - type: Relative - value: 0.25 -PowerLaw: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - slope: - limits: - lower: 1.0 - upper: 3.0 - lower_limit: 1.5 - type: Uniform - upper_limit: 3.0 - width_modifier: - type: Absolute - value: 0.2 -PowerLawCore: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - core_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 0.2 - width_modifier: - type: Absolute - value: 0.1 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 - ell_comps_0: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - ell_comps_1: - limits: - lower: -1.0 - upper: 1.0 - lower_limit: -1.0 - mean: 0.0 - sigma: 0.3 - type: TruncatedGaussian - upper_limit: 1.0 - width_modifier: - type: Absolute - value: 0.2 - slope: - limits: - lower: 1.0 - upper: 3.0 - lower_limit: 1.5 - type: Uniform - upper_limit: 3.0 - width_modifier: - type: Absolute - value: 0.2 -PowerLawCoreSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - core_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 0.2 - width_modifier: - type: Absolute - value: 0.1 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 - slope: - limits: - lower: 1.0 - upper: 3.0 - lower_limit: 1.5 - type: Uniform - upper_limit: 3.0 - width_modifier: - type: Absolute - value: 0.2 -PowerLawSph: - centre_0: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - centre_1: - limits: - lower: -inf - upper: inf - mean: 0.0 - sigma: 0.3 - type: Gaussian - width_modifier: - type: Absolute - value: 0.05 - einstein_radius: - limits: - lower: 0.0 - upper: inf - lower_limit: 0.0 - type: Uniform - upper_limit: 4.0 - width_modifier: - type: Relative - value: 0.05 - slope: - limits: - lower: 1.0 - upper: 3.0 - lower_limit: 1.5 - type: Uniform - upper_limit: 3.0 - width_modifier: - type: Absolute - value: 0.2 +Isothermal: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 +IsothermalCore: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + core_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 0.2 + width_modifier: + type: Absolute + value: 0.1 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 +IsothermalCoreSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + core_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 0.2 + width_modifier: + type: Absolute + value: 0.1 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 +IsothermalSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 +PointMass: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 8.0 + width_modifier: + type: Relative + value: 0.25 +PowerLaw: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + slope: + limits: + lower: 1.0 + upper: 3.0 + lower_limit: 1.5 + type: Uniform + upper_limit: 3.0 + width_modifier: + type: Absolute + value: 0.2 +PowerLawCore: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + core_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 0.2 + width_modifier: + type: Absolute + value: 0.1 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 + ell_comps_0: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + ell_comps_1: + limits: + lower: -1.0 + upper: 1.0 + lower_limit: -1.0 + mean: 0.0 + sigma: 0.3 + type: TruncatedGaussian + upper_limit: 1.0 + width_modifier: + type: Absolute + value: 0.2 + slope: + limits: + lower: 1.0 + upper: 3.0 + lower_limit: 1.5 + type: Uniform + upper_limit: 3.0 + width_modifier: + type: Absolute + value: 0.2 +PowerLawCoreSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + core_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 0.2 + width_modifier: + type: Absolute + value: 0.1 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 + slope: + limits: + lower: 1.0 + upper: 3.0 + lower_limit: 1.5 + type: Uniform + upper_limit: 3.0 + width_modifier: + type: Absolute + value: 0.2 +PowerLawSph: + centre_0: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + centre_1: + limits: + lower: -inf + upper: inf + mean: 0.0 + sigma: 0.3 + type: Gaussian + width_modifier: + type: Absolute + value: 0.05 + einstein_radius: + limits: + lower: 0.0 + upper: inf + lower_limit: 0.0 + type: Uniform + upper_limit: 4.0 + width_modifier: + type: Relative + value: 0.05 + slope: + limits: + lower: 1.0 + upper: 3.0 + lower_limit: 1.5 + type: Uniform + upper_limit: 3.0 + width_modifier: + type: Absolute + value: 0.2 diff --git a/test_autolens/conftest.py b/test_autolens/conftest.py index 1fc585d38..6e252649d 100644 --- a/test_autolens/conftest.py +++ b/test_autolens/conftest.py @@ -1,368 +1,368 @@ -import os -from pathlib import Path -from unittest.mock import MagicMock - -import pytest -from matplotlib import pyplot -import matplotlib.figure - -from autofit import conf -from autolens import fixtures - -import logging - -logger = logging.getLogger(__name__) - -logger.setLevel(level="INFO") - - -class PlotPatch: - def __init__(self): - self.paths = [] - - def __call__(self, path, *args, **kwargs): - self.paths.append(str(path)) - - -@pytest.fixture(name="plot_patch") -def make_plot_patch(monkeypatch): - plot_patch = PlotPatch() - monkeypatch.setattr(pyplot, "savefig", plot_patch) - monkeypatch.setattr(matplotlib.figure.Figure, "savefig", plot_patch) - monkeypatch.setattr(pyplot, "tight_layout", lambda *a, **kw: None) - monkeypatch.setattr(pyplot, "colorbar", lambda *a, **kw: MagicMock()) - return plot_patch - - -directory = Path(__file__).resolve().parent - - -@pytest.fixture(autouse=True) -def set_config_path(request): - conf.instance.push( - new_path=directory / "config", - output_path=directory / "output", - ) - - -@pytest.fixture(autouse=True, scope="session") -def remove_logs(): - yield - for d, _, files in os.walk(directory): - for file in files: - if file.endswith(".log"): - os.remove(Path(d) / file) - - -############ -# AutoLens # -############ - -# Lens Datasets # - - -@pytest.fixture(name="mask_2d_7x7") -def make_mask_2d_7x7(): - return fixtures.make_mask_2d_7x7() - - -@pytest.fixture(name="sub_mask_2d_7x7") -def make_sub_mask_2d_7x7(): - return fixtures.make_sub_mask_2d_7x7() - - -@pytest.fixture(name="mask_2d_7x7_1_pix") -def make_mask_2d_7x7_1_pix(): - return fixtures.make_mask_2d_7x7_1_pix() - - -@pytest.fixture(name="blurring_mask_2d_7x7") -def make_blurring_mask_2d_7x7(): - return fixtures.make_blurring_mask_2d_7x7() - - -@pytest.fixture(name="grid_2d_7x7") -def make_grid_2d_7x7(): - return fixtures.make_grid_2d_7x7() - - -@pytest.fixture(name="grid_2d_irregular_7x7") -def make_grid_2d_irregular_7x7(): - return fixtures.make_grid_2d_irregular_7x7() - - -@pytest.fixture(name="grid_2d_irregular_7x7_list") -def make_grid_2d_irregular_7x7_list(): - return fixtures.make_grid_2d_irregular_7x7_list() - - -@pytest.fixture(name="blurring_grid_2d_7x7") -def make_blurring_grid_2d_7x7(blurring_mask_2d_7x7): - return fixtures.make_blurring_grid_2d_7x7() - - -@pytest.fixture(name="image_7x7") -def make_image_7x7(): - return fixtures.make_image_7x7() - - -@pytest.fixture(name="noise_map_7x7") -def make_noise_map_7x7(): - return fixtures.make_noise_map_7x7() - - -@pytest.fixture(name="psf_3x3") -def make_psf_3x3(): - return fixtures.make_psf_3x3() - - -@pytest.fixture(name="psf_3x3") -def make_psf_3x3(): - return fixtures.make_psf_3x3() - - -@pytest.fixture(name="imaging_7x7") -def make_imaging_7x7(): - return fixtures.make_imaging_7x7() - - -@pytest.fixture(name="masked_imaging_7x7") -def make_masked_imaging_7x7(): - return fixtures.make_masked_imaging_7x7() - - -@pytest.fixture(name="masked_imaging_7x7_sub_2") -def make_masked_imaging_7x7_sub_2(): - return fixtures.make_masked_imaging_7x7_sub_2() - - -@pytest.fixture(name="masked_imaging_covariance_7x7") -def make_masked_imaging_covariance_7x7(): - return fixtures.make_masked_imaging_covariance_7x7() - - -@pytest.fixture(name="masked_imaging_7x7_no_blur") -def make_masked_imaging_7x7_no_blur(): - return fixtures.make_masked_imaging_7x7_no_blur() - - -@pytest.fixture(name="masked_imaging_7x7_no_blur_sub_2") -def make_masked_imaging_7x7_no_blur_sub_2(): - return fixtures.make_masked_imaging_7x7_no_blur_sub_2() - - -@pytest.fixture(name="visibilities_7") -def make_visibilities_7(): - return fixtures.make_visibilities_7() - - -@pytest.fixture(name="visibilities_noise_map_7") -def make_noise_map_7(): - return fixtures.make_visibilities_noise_map_7() - - -@pytest.fixture(name="uv_wavelengths_7x2") -def make_uv_wavelengths_7x2(): - return fixtures.make_uv_wavelengths_7x2() - - -@pytest.fixture(name="transformer_7x7_7") -def make_transformer_7x7_7(): - return fixtures.make_transformer_7x7_7() - - -@pytest.fixture(name="interferometer_7") -def make_interferometer_7(): - return fixtures.make_interferometer_7() - - -@pytest.fixture(name="interferometer_7_lop") -def make_interferometer_7_lop(): - return fixtures.make_interferometer_7_lop() - - -@pytest.fixture(name="interferometer_7_grid") -def make_interferometer_7_grid(): - return fixtures.make_interferometer_7_grid() - - -@pytest.fixture(name="positions_x2") -def make_positions_x2(): - return fixtures.make_positions_x2() - - -@pytest.fixture(name="positions_x2_noise_map") -def make_positions_x2_noise_map(): - return fixtures.make_positions_noise_map_x2() - - -@pytest.fixture(name="fluxes_x2") -def make_fluxes_x2(): - return fixtures.make_fluxes_x2() - - -@pytest.fixture(name="fluxes_x2_noise_map") -def make_fluxes_x2_noise_map(): - return fixtures.make_fluxes_noise_map_x2() - - -@pytest.fixture(name="point_dataset") -def make_point_dataset(): - return fixtures.make_point_dataset() - - -@pytest.fixture(name="point_dict") -def make_point_dict(): - return fixtures.make_point_dict() - - -# galaxies # - - -@pytest.fixture(name="ps_0") -def make_ps_0(): - return fixtures.make_ps_0() - - -@pytest.fixture(name="ps_1") -def make_ps_1(): - return fixtures.make_ps_1() - - -@pytest.fixture(name="lp_0") -def make_lp_0(): - return fixtures.make_lp_0() - - -@pytest.fixture(name="lp_operated_0") -def make_lp_operated_0(): - return fixtures.make_lp_operated_0() - - -@pytest.fixture(name="mp_0") -def make_mp_0(): - return fixtures.make_mp_0() - - -@pytest.fixture(name="gal_x1_mp") -def make_gal_x1_mp(): - return fixtures.make_gal_x1_mp() - - -@pytest.fixture(name="gal_x1_lp") -def make_gal_x1_lp(): - return fixtures.make_gal_x1_lp() - - -# Ray Tracing # - - -@pytest.fixture(name="grid_2d_7x7_simple") -def make_grid_2d_7x7_simple(): - return fixtures.make_grid_2d_7x7_simple() - - -@pytest.fixture(name="tracer_x1_plane_7x7") -def make_tracer_x1_plane_7x7(gal_x1_lp): - return fixtures.make_tracer_x1_plane_7x7() - - -@pytest.fixture(name="tracer_x2_plane_7x7") -def make_tracer_x2_plane_7x7(): - return fixtures.make_tracer_x2_plane_7x7() - - -@pytest.fixture(name="tracer_x2_plane_inversion_7x7") -def make_tracer_x2_plane_inversion_7x7(): - return fixtures.make_tracer_x2_plane_inversion_7x7() - - -@pytest.fixture(name="tracer_x2_plane_point") -def make_tracer_x2_plane_point(): - return fixtures.make_tracer_x2_plane_point() - - -@pytest.fixture(name="Planck15") -def make_Planck15(): - return fixtures.make_Planck15() - - -# Lens Fit # - - -@pytest.fixture(name="fit_imaging_x1_plane_7x7") -def make_fit_imaging_x1_plane_7x7(): - return fixtures.make_fit_imaging_x1_plane_7x7() - - -@pytest.fixture(name="fit_imaging_x2_plane_7x7") -def make_fit_imaging_x2_plane_7x7(): - return fixtures.make_fit_imaging_x2_plane_7x7() - - -@pytest.fixture(name="fit_imaging_x2_plane_inversion_7x7") -def make_fit_imaging_x2_plane_inversion_7x7(): - return fixtures.make_fit_imaging_x2_plane_inversion_7x7() - - -@pytest.fixture(name="fit_interferometer_x1_plane_7x7") -def make_fit_interferometer_x1_plane_7x7(): - return fixtures.make_fit_interferometer_x1_plane_7x7() - - -@pytest.fixture(name="fit_interferometer_x2_plane_7x7") -def make_fit_interferometer_x2_plane_7x7(): - return fixtures.make_fit_interferometer_x2_plane_7x7() - - -@pytest.fixture(name="fit_interferometer_x2_plane_inversion_7x7") -def make_fit_interferometer_x2_plane_inversion_7x7(): - return fixtures.make_fit_interferometer_x2_plane_inversion_7x7() - - -@pytest.fixture(name="fit_point_dataset_x2_plane") -def make_fit_point_dataset_x2_plane(): - return fixtures.make_fit_point_dataset_x2_plane() - - -@pytest.fixture(name="fit_point_dict_x2_plane") -def make_fit_point_dict_x2_plane(): - return fixtures.make_fit_point_dict_x2_plane() - - -### Analysis ### - - -@pytest.fixture(name="analysis_imaging_7x7") -def make_analysis_imaging_7x7(): - return fixtures.make_analysis_imaging_7x7() - - -@pytest.fixture(name="analysis_interferometer_7") -def make_analysis_interferometer_7(): - return fixtures.make_analysis_interferometer_7() - - -@pytest.fixture(name="analysis_point_x2") -def make_analysis_point_x2(): - return fixtures.make_analysis_point_x2() - - -@pytest.fixture(name="adapt_galaxy_image_0_7x7") -def make_adapt_galaxy_image_0_7x7(): - return fixtures.make_adapt_galaxy_image_0_7x7() - - -@pytest.fixture(name="adapt_galaxy_name_image_dict_7x7") -def make_adapt_galaxy_name_image_dict_7x7(): - return fixtures.make_adapt_galaxy_name_image_dict_7x7() - - -@pytest.fixture(name="adapt_images_7x7") -def make_adapt_images_7x7(): - return fixtures.make_adapt_images_7x7() - - -@pytest.fixture(name="samples_summary_with_result") -def make_samples_summary_with_result(): - return fixtures.make_samples_summary_with_result() +import os +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from matplotlib import pyplot +import matplotlib.figure + +from autofit import conf +from autolens import fixtures + +import logging + +logger = logging.getLogger(__name__) + +logger.setLevel(level="INFO") + + +class PlotPatch: + def __init__(self): + self.paths = [] + + def __call__(self, path, *args, **kwargs): + self.paths.append(str(path)) + + +@pytest.fixture(name="plot_patch") +def make_plot_patch(monkeypatch): + plot_patch = PlotPatch() + monkeypatch.setattr(pyplot, "savefig", plot_patch) + monkeypatch.setattr(matplotlib.figure.Figure, "savefig", plot_patch) + monkeypatch.setattr(pyplot, "tight_layout", lambda *a, **kw: None) + monkeypatch.setattr(pyplot, "colorbar", lambda *a, **kw: MagicMock()) + return plot_patch + + +directory = Path(__file__).resolve().parent + + +@pytest.fixture(autouse=True) +def set_config_path(request): + conf.instance.push( + new_path=directory / "config", + output_path=directory / "output", + ) + + +@pytest.fixture(autouse=True, scope="session") +def remove_logs(): + yield + for d, _, files in os.walk(directory): + for file in files: + if file.endswith(".log"): + os.remove(Path(d) / file) + + +############ +# AutoLens # +############ + +# Lens Datasets # + + +@pytest.fixture(name="mask_2d_7x7") +def make_mask_2d_7x7(): + return fixtures.make_mask_2d_7x7() + + +@pytest.fixture(name="sub_mask_2d_7x7") +def make_sub_mask_2d_7x7(): + return fixtures.make_sub_mask_2d_7x7() + + +@pytest.fixture(name="mask_2d_7x7_1_pix") +def make_mask_2d_7x7_1_pix(): + return fixtures.make_mask_2d_7x7_1_pix() + + +@pytest.fixture(name="blurring_mask_2d_7x7") +def make_blurring_mask_2d_7x7(): + return fixtures.make_blurring_mask_2d_7x7() + + +@pytest.fixture(name="grid_2d_7x7") +def make_grid_2d_7x7(): + return fixtures.make_grid_2d_7x7() + + +@pytest.fixture(name="grid_2d_irregular_7x7") +def make_grid_2d_irregular_7x7(): + return fixtures.make_grid_2d_irregular_7x7() + + +@pytest.fixture(name="grid_2d_irregular_7x7_list") +def make_grid_2d_irregular_7x7_list(): + return fixtures.make_grid_2d_irregular_7x7_list() + + +@pytest.fixture(name="blurring_grid_2d_7x7") +def make_blurring_grid_2d_7x7(blurring_mask_2d_7x7): + return fixtures.make_blurring_grid_2d_7x7() + + +@pytest.fixture(name="image_7x7") +def make_image_7x7(): + return fixtures.make_image_7x7() + + +@pytest.fixture(name="noise_map_7x7") +def make_noise_map_7x7(): + return fixtures.make_noise_map_7x7() + + +@pytest.fixture(name="psf_3x3") +def make_psf_3x3(): + return fixtures.make_psf_3x3() + + +@pytest.fixture(name="psf_3x3") +def make_psf_3x3(): + return fixtures.make_psf_3x3() + + +@pytest.fixture(name="imaging_7x7") +def make_imaging_7x7(): + return fixtures.make_imaging_7x7() + + +@pytest.fixture(name="masked_imaging_7x7") +def make_masked_imaging_7x7(): + return fixtures.make_masked_imaging_7x7() + + +@pytest.fixture(name="masked_imaging_7x7_sub_2") +def make_masked_imaging_7x7_sub_2(): + return fixtures.make_masked_imaging_7x7_sub_2() + + +@pytest.fixture(name="masked_imaging_covariance_7x7") +def make_masked_imaging_covariance_7x7(): + return fixtures.make_masked_imaging_covariance_7x7() + + +@pytest.fixture(name="masked_imaging_7x7_no_blur") +def make_masked_imaging_7x7_no_blur(): + return fixtures.make_masked_imaging_7x7_no_blur() + + +@pytest.fixture(name="masked_imaging_7x7_no_blur_sub_2") +def make_masked_imaging_7x7_no_blur_sub_2(): + return fixtures.make_masked_imaging_7x7_no_blur_sub_2() + + +@pytest.fixture(name="visibilities_7") +def make_visibilities_7(): + return fixtures.make_visibilities_7() + + +@pytest.fixture(name="visibilities_noise_map_7") +def make_noise_map_7(): + return fixtures.make_visibilities_noise_map_7() + + +@pytest.fixture(name="uv_wavelengths_7x2") +def make_uv_wavelengths_7x2(): + return fixtures.make_uv_wavelengths_7x2() + + +@pytest.fixture(name="transformer_7x7_7") +def make_transformer_7x7_7(): + return fixtures.make_transformer_7x7_7() + + +@pytest.fixture(name="interferometer_7") +def make_interferometer_7(): + return fixtures.make_interferometer_7() + + +@pytest.fixture(name="interferometer_7_lop") +def make_interferometer_7_lop(): + return fixtures.make_interferometer_7_lop() + + +@pytest.fixture(name="interferometer_7_grid") +def make_interferometer_7_grid(): + return fixtures.make_interferometer_7_grid() + + +@pytest.fixture(name="positions_x2") +def make_positions_x2(): + return fixtures.make_positions_x2() + + +@pytest.fixture(name="positions_x2_noise_map") +def make_positions_x2_noise_map(): + return fixtures.make_positions_noise_map_x2() + + +@pytest.fixture(name="fluxes_x2") +def make_fluxes_x2(): + return fixtures.make_fluxes_x2() + + +@pytest.fixture(name="fluxes_x2_noise_map") +def make_fluxes_x2_noise_map(): + return fixtures.make_fluxes_noise_map_x2() + + +@pytest.fixture(name="point_dataset") +def make_point_dataset(): + return fixtures.make_point_dataset() + + +@pytest.fixture(name="point_dict") +def make_point_dict(): + return fixtures.make_point_dict() + + +# galaxies # + + +@pytest.fixture(name="ps_0") +def make_ps_0(): + return fixtures.make_ps_0() + + +@pytest.fixture(name="ps_1") +def make_ps_1(): + return fixtures.make_ps_1() + + +@pytest.fixture(name="lp_0") +def make_lp_0(): + return fixtures.make_lp_0() + + +@pytest.fixture(name="lp_operated_0") +def make_lp_operated_0(): + return fixtures.make_lp_operated_0() + + +@pytest.fixture(name="mp_0") +def make_mp_0(): + return fixtures.make_mp_0() + + +@pytest.fixture(name="gal_x1_mp") +def make_gal_x1_mp(): + return fixtures.make_gal_x1_mp() + + +@pytest.fixture(name="gal_x1_lp") +def make_gal_x1_lp(): + return fixtures.make_gal_x1_lp() + + +# Ray Tracing # + + +@pytest.fixture(name="grid_2d_7x7_simple") +def make_grid_2d_7x7_simple(): + return fixtures.make_grid_2d_7x7_simple() + + +@pytest.fixture(name="tracer_x1_plane_7x7") +def make_tracer_x1_plane_7x7(gal_x1_lp): + return fixtures.make_tracer_x1_plane_7x7() + + +@pytest.fixture(name="tracer_x2_plane_7x7") +def make_tracer_x2_plane_7x7(): + return fixtures.make_tracer_x2_plane_7x7() + + +@pytest.fixture(name="tracer_x2_plane_inversion_7x7") +def make_tracer_x2_plane_inversion_7x7(): + return fixtures.make_tracer_x2_plane_inversion_7x7() + + +@pytest.fixture(name="tracer_x2_plane_point") +def make_tracer_x2_plane_point(): + return fixtures.make_tracer_x2_plane_point() + + +@pytest.fixture(name="Planck15") +def make_Planck15(): + return fixtures.make_Planck15() + + +# Lens Fit # + + +@pytest.fixture(name="fit_imaging_x1_plane_7x7") +def make_fit_imaging_x1_plane_7x7(): + return fixtures.make_fit_imaging_x1_plane_7x7() + + +@pytest.fixture(name="fit_imaging_x2_plane_7x7") +def make_fit_imaging_x2_plane_7x7(): + return fixtures.make_fit_imaging_x2_plane_7x7() + + +@pytest.fixture(name="fit_imaging_x2_plane_inversion_7x7") +def make_fit_imaging_x2_plane_inversion_7x7(): + return fixtures.make_fit_imaging_x2_plane_inversion_7x7() + + +@pytest.fixture(name="fit_interferometer_x1_plane_7x7") +def make_fit_interferometer_x1_plane_7x7(): + return fixtures.make_fit_interferometer_x1_plane_7x7() + + +@pytest.fixture(name="fit_interferometer_x2_plane_7x7") +def make_fit_interferometer_x2_plane_7x7(): + return fixtures.make_fit_interferometer_x2_plane_7x7() + + +@pytest.fixture(name="fit_interferometer_x2_plane_inversion_7x7") +def make_fit_interferometer_x2_plane_inversion_7x7(): + return fixtures.make_fit_interferometer_x2_plane_inversion_7x7() + + +@pytest.fixture(name="fit_point_dataset_x2_plane") +def make_fit_point_dataset_x2_plane(): + return fixtures.make_fit_point_dataset_x2_plane() + + +@pytest.fixture(name="fit_point_dict_x2_plane") +def make_fit_point_dict_x2_plane(): + return fixtures.make_fit_point_dict_x2_plane() + + +### Analysis ### + + +@pytest.fixture(name="analysis_imaging_7x7") +def make_analysis_imaging_7x7(): + return fixtures.make_analysis_imaging_7x7() + + +@pytest.fixture(name="analysis_interferometer_7") +def make_analysis_interferometer_7(): + return fixtures.make_analysis_interferometer_7() + + +@pytest.fixture(name="analysis_point_x2") +def make_analysis_point_x2(): + return fixtures.make_analysis_point_x2() + + +@pytest.fixture(name="adapt_galaxy_image_0_7x7") +def make_adapt_galaxy_image_0_7x7(): + return fixtures.make_adapt_galaxy_image_0_7x7() + + +@pytest.fixture(name="adapt_galaxy_name_image_dict_7x7") +def make_adapt_galaxy_name_image_dict_7x7(): + return fixtures.make_adapt_galaxy_name_image_dict_7x7() + + +@pytest.fixture(name="adapt_images_7x7") +def make_adapt_images_7x7(): + return fixtures.make_adapt_images_7x7() + + +@pytest.fixture(name="samples_summary_with_result") +def make_samples_summary_with_result(): + return fixtures.make_samples_summary_with_result() diff --git a/test_autolens/imaging/model/test_analysis_imaging.py b/test_autolens/imaging/model/test_analysis_imaging.py index e5d51ac73..7549686c9 100644 --- a/test_autolens/imaging/model/test_analysis_imaging.py +++ b/test_autolens/imaging/model/test_analysis_imaging.py @@ -1,299 +1,299 @@ -from pathlib import Path -import pytest - -import autofit as af - -import autolens as al - -from autolens.imaging.model.result import ResultImaging - -from autolens import exc - - -directory = Path(__file__).resolve().parent - - -def test__make_result__result_imaging_is_returned(masked_imaging_7x7): - - model = af.Collection(galaxies=af.Collection(galaxy_0=al.Galaxy(redshift=0.5))) - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) - - search = al.m.MockSearch(name="test_search") - - result = search.fit(model=model, analysis=analysis) - - assert isinstance(result, ResultImaging) - - -def test__figure_of_merit__matches_correct_fit_given_galaxy_profiles( - masked_imaging_7x7, -): - lens = al.Galaxy(redshift=0.5, light=al.lp.Sersic(intensity=0.1)) - - model = af.Collection(galaxies=af.Collection(lens=lens)) - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) - instance = model.instance_from_unit_vector([]) - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - tracer = analysis.tracer_via_instance_from(instance=instance) - - fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer) - - assert fit.log_likelihood == analysis_log_likelihood - - -def test__log_likelihood_function__returns_figure_of_merit_for_pixelization( - masked_imaging_7x7, -): - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=1.0), - ) - - lens = al.Galaxy( - redshift=0.5, - mass=al.mp.IsothermalSph(einstein_radius=1.0), - ) - source = al.Galaxy(redshift=1.0, pixelization=pixelization) - - model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) - instance = model.instance_from_unit_vector([]) - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - tracer = analysis.tracer_via_instance_from(instance=instance) - fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer) - - assert analysis_log_likelihood == pytest.approx(fit.figure_of_merit) - assert fit.figure_of_merit != pytest.approx(fit.log_likelihood, rel=1e-6) - - -def test__positions__likelihood_overwrites__changes_likelihood(masked_imaging_7x7): - lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph(centre=(0.05, 0.05))) - source = al.Galaxy(redshift=1.0, light=al.lp.SersicSph(centre=(0.05, 0.05))) - - model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) - - instance = model.instance_from_unit_vector([]) - - analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - tracer = analysis.tracer_via_instance_from(instance=instance) - - fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer) - - assert fit.log_likelihood == pytest.approx(analysis_log_likelihood, 1.0e-4) - assert analysis_log_likelihood == pytest.approx(-14.79034680979, 1.0e-4) - - positions_likelihood = al.PositionsLH( - positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 - ) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, positions_likelihood_list=[positions_likelihood], use_jax=False - ) - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - assert analysis_log_likelihood == pytest.approx(-44097289521.734665, 1.0e-4) - - -def test__positions__likelihood_overwrites__changes_likelihood__double_source_plane_example(masked_imaging_7x7): - - lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph(centre=(0.05, 0.05))) - source_0 = al.Galaxy(redshift=1.0, light=al.lp.SersicSph(centre=(0.05, 0.05))) - source_1 = al.Galaxy(redshift=2.0, light=al.lp.SersicSph(centre=(0.05, 0.05))) - - model = af.Collection(galaxies=af.Collection(lens=lens, source_0=source_0, source_1=source_1)) - - instance = model.instance_from_unit_vector([]) - - positions_likelihood_0 = al.PositionsLH( - plane_redshift=1.0, positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 - ) - positions_likelihood_1 = al.PositionsLH( - plane_redshift=2.0, positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 - ) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, positions_likelihood_list=[positions_likelihood_0, positions_likelihood_1], use_jax=False - ) - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - assert analysis_log_likelihood == pytest.approx(-44097289521.734665, 1.0e-4) - - - -def _shared_mesh_analysis(masked_imaging_7x7, shared_preloads): - """ - An `AnalysisImaging` with an image-mesh (`Overlay`) + `Delaunay` pixelization, the regime the - multi-exposure shared-state path applies to (the source-plane mesh is traced from image-plane - mesh centres, so it can be shared across exposures). - """ - import autoarray as aa - - lens = al.Galaxy( - redshift=0.5, - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), - ) - - pixelization = al.Pixelization( - mesh=al.mesh.Delaunay(pixels=9, zeroed_pixels=0), - regularization=al.reg.Constant(coefficient=0.01), - ) - - source = al.Galaxy(redshift=1.0, pixelization=pixelization) - - image_mesh = al.image_mesh.Overlay(shape=(3, 3)) - image_plane_mesh_grid = image_mesh.image_plane_mesh_grid_from( - mask=masked_imaging_7x7.mask, - ) - - adapt_images = al.AdaptImages( - galaxy_name_image_dict={ - str(("galaxies", "source")): masked_imaging_7x7.data, - }, - galaxy_name_image_plane_mesh_grid_dict={ - str(("galaxies", "source")): image_plane_mesh_grid, - }, - ) - - model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, - adapt_images=adapt_images, - use_jax=False, - shared_preloads=shared_preloads, - raise_inversion_positions_likelihood_exception=False, - ) - - return model, analysis - - -def test__shared_state_from__mesh_reused__figure_of_merit_unchanged( - masked_imaging_7x7, -): - import autoarray as aa - - model, analysis = _shared_mesh_analysis(masked_imaging_7x7, shared_preloads=True) - instance = model.instance_from_unit_vector([]) - - # `shared_state_from` builds a `PreloadsImaging` carrying the source-plane mesh geometry (the - # exposure-invariant quantity) — NOT the mapper / curvature matrix / regularization matrix, - # which are per-exposure for imaging (PSFs, offsets, adaptive regularization). - shared = analysis.shared_state_from(instance=instance) - assert isinstance(shared, aa.PreloadsImaging) - assert shared.source_plane_mesh_grid is not None - assert shared.image_plane_mesh_grid is not None - assert shared.curvature_matrix is None - assert shared.mapper_galaxy_dict is None - - # The preloaded mesh is reused by the fit (identity) and leaves the figure of merit unchanged. - fit_unshared = analysis.fit_from(instance=instance) - fom_unshared = fit_unshared.figure_of_merit - - fit_shared = analysis.fit_from(instance=instance, preloads=shared) - - assert ( - fit_shared.tracer_to_inversion.traced_mesh_grid_pg_list - is shared.source_plane_mesh_grid - ) - assert fit_shared.figure_of_merit == pytest.approx(fom_unshared) - - # The full `log_likelihood_function` with the shared object matches the unshared call. - assert analysis.log_likelihood_function( - instance=instance, shared=shared - ) == pytest.approx(analysis.log_likelihood_function(instance=instance)) - - -def test__shared_state_from__returns_none_when_not_opted_in(masked_imaging_7x7): - model, analysis = _shared_mesh_analysis(masked_imaging_7x7, shared_preloads=False) - instance = model.instance_from_unit_vector([]) - - assert analysis.shared_state_from(instance=instance) is None - - -def test__shared_state_from__returns_none_when_no_inversion(masked_imaging_7x7): - lens = al.Galaxy(redshift=0.5, light=al.lp.Sersic(intensity=0.1)) - - model = af.Collection(galaxies=af.Collection(lens=lens)) - instance = model.instance_from_unit_vector([]) - - analysis = al.AnalysisImaging( - dataset=masked_imaging_7x7, use_jax=False, shared_preloads=True - ) - - assert analysis.shared_state_from(instance=instance) is None - - -def _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads): - factors = [] - model = None - for _ in range(2): - model, analysis = _shared_mesh_analysis(masked_imaging_7x7, shared_preloads) - factors.append(af.AnalysisFactor(prior_model=model.copy(), analysis=analysis)) - - factor_graph = af.FactorGraphModel(*factors) - instance = factor_graph.global_prior_model.instance_from_unit_vector([]) - return factor_graph.log_likelihood_function(instance) - - -def test__factor_graph__shared_vs_unshared_equal(masked_imaging_7x7): - ll_unshared = _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads=False) - ll_shared = _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads=True) - - print(f"\nunshared={ll_unshared} shared={ll_shared}") - assert ll_shared == pytest.approx(ll_unshared, rel=1e-10) - - -def test__factor_graph__shared_state_computed_once(masked_imaging_7x7, monkeypatch): - calls = {"n": 0} - - original = al.AnalysisImaging.shared_state_from - - def counting(self, instance): - result = original(self, instance) - if result is not None: - calls["n"] += 1 - return result - - monkeypatch.setattr(al.AnalysisImaging, "shared_state_from", counting) - - _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads=True) - - assert calls["n"] == 1 - - -def test__preloads_scoped__cross_type_preloads_reduced_to_mesh_view(masked_imaging_7x7): - import autoarray as aa - - lens = al.Galaxy(redshift=0.5, light=al.lp.Sersic(intensity=0.1)) - tracer = al.Tracer(galaxies=[lens]) - - # Cross-dataset-type preloads (e.g. from an interferometer lead factor in a joint graph): - # the mapper / curvature matrix embed the other dataset's grids and must NOT be consumed - # by an imaging fit — only the mesh-geometry view survives the scoping. - cross_type = aa.PreloadsInterferometer( - curvature_matrix="other-datasets-F", - mapper_galaxy_dict="other-datasets-mapper", - source_plane_mesh_grid=[["mesh"]], - image_plane_mesh_grid=[["image-mesh"]], - ) - - fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer, preloads=cross_type) - - scoped = fit._preloads_scoped - assert isinstance(scoped, aa.PreloadsImaging) - assert scoped.source_plane_mesh_grid == [["mesh"]] - assert scoped.image_plane_mesh_grid == [["image-mesh"]] - assert scoped.curvature_matrix is None - assert scoped.mapper_galaxy_dict is None - - # Same-type preloads pass through untouched. - same_type = aa.PreloadsImaging(source_plane_mesh_grid=[["mesh"]]) - fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer, preloads=same_type) - assert fit._preloads_scoped is same_type +from pathlib import Path +import pytest + +import autofit as af + +import autolens as al + +from autolens.imaging.model.result import ResultImaging + +from autolens import exc + + +directory = Path(__file__).resolve().parent + + +def test__make_result__result_imaging_is_returned(masked_imaging_7x7): + + model = af.Collection(galaxies=af.Collection(galaxy_0=al.Galaxy(redshift=0.5))) + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) + + search = al.m.MockSearch(name="test_search") + + result = search.fit(model=model, analysis=analysis) + + assert isinstance(result, ResultImaging) + + +def test__figure_of_merit__matches_correct_fit_given_galaxy_profiles( + masked_imaging_7x7, +): + lens = al.Galaxy(redshift=0.5, light=al.lp.Sersic(intensity=0.1)) + + model = af.Collection(galaxies=af.Collection(lens=lens)) + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) + instance = model.instance_from_unit_vector([]) + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + tracer = analysis.tracer_via_instance_from(instance=instance) + + fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer) + + assert fit.log_likelihood == analysis_log_likelihood + + +def test__log_likelihood_function__returns_figure_of_merit_for_pixelization( + masked_imaging_7x7, +): + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=1.0), + ) + + lens = al.Galaxy( + redshift=0.5, + mass=al.mp.IsothermalSph(einstein_radius=1.0), + ) + source = al.Galaxy(redshift=1.0, pixelization=pixelization) + + model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) + instance = model.instance_from_unit_vector([]) + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + tracer = analysis.tracer_via_instance_from(instance=instance) + fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer) + + assert analysis_log_likelihood == pytest.approx(fit.figure_of_merit) + assert fit.figure_of_merit != pytest.approx(fit.log_likelihood, rel=1e-6) + + +def test__positions__likelihood_overwrites__changes_likelihood(masked_imaging_7x7): + lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph(centre=(0.05, 0.05))) + source = al.Galaxy(redshift=1.0, light=al.lp.SersicSph(centre=(0.05, 0.05))) + + model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) + + instance = model.instance_from_unit_vector([]) + + analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False) + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + tracer = analysis.tracer_via_instance_from(instance=instance) + + fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer) + + assert fit.log_likelihood == pytest.approx(analysis_log_likelihood, 1.0e-4) + assert analysis_log_likelihood == pytest.approx(-14.79034680979, 1.0e-4) + + positions_likelihood = al.PositionsLH( + positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 + ) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, positions_likelihood_list=[positions_likelihood], use_jax=False + ) + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + assert analysis_log_likelihood == pytest.approx(-44097289521.734665, 1.0e-4) + + +def test__positions__likelihood_overwrites__changes_likelihood__double_source_plane_example(masked_imaging_7x7): + + lens = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph(centre=(0.05, 0.05))) + source_0 = al.Galaxy(redshift=1.0, light=al.lp.SersicSph(centre=(0.05, 0.05))) + source_1 = al.Galaxy(redshift=2.0, light=al.lp.SersicSph(centre=(0.05, 0.05))) + + model = af.Collection(galaxies=af.Collection(lens=lens, source_0=source_0, source_1=source_1)) + + instance = model.instance_from_unit_vector([]) + + positions_likelihood_0 = al.PositionsLH( + plane_redshift=1.0, positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 + ) + positions_likelihood_1 = al.PositionsLH( + plane_redshift=2.0, positions=al.Grid2DIrregular([(1.0, 100.0), (200.0, 2.0)]), threshold=0.01 + ) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, positions_likelihood_list=[positions_likelihood_0, positions_likelihood_1], use_jax=False + ) + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + assert analysis_log_likelihood == pytest.approx(-44097289521.734665, 1.0e-4) + + + +def _shared_mesh_analysis(masked_imaging_7x7, shared_preloads): + """ + An `AnalysisImaging` with an image-mesh (`Overlay`) + `Delaunay` pixelization, the regime the + multi-exposure shared-state path applies to (the source-plane mesh is traced from image-plane + mesh centres, so it can be shared across exposures). + """ + import autoarray as aa + + lens = al.Galaxy( + redshift=0.5, + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), + ) + + pixelization = al.Pixelization( + mesh=al.mesh.Delaunay(pixels=9, zeroed_pixels=0), + regularization=al.reg.Constant(coefficient=0.01), + ) + + source = al.Galaxy(redshift=1.0, pixelization=pixelization) + + image_mesh = al.image_mesh.Overlay(shape=(3, 3)) + image_plane_mesh_grid = image_mesh.image_plane_mesh_grid_from( + mask=masked_imaging_7x7.mask, + ) + + adapt_images = al.AdaptImages( + galaxy_name_image_dict={ + str(("galaxies", "source")): masked_imaging_7x7.data, + }, + galaxy_name_image_plane_mesh_grid_dict={ + str(("galaxies", "source")): image_plane_mesh_grid, + }, + ) + + model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, + adapt_images=adapt_images, + use_jax=False, + shared_preloads=shared_preloads, + raise_inversion_positions_likelihood_exception=False, + ) + + return model, analysis + + +def test__shared_state_from__mesh_reused__figure_of_merit_unchanged( + masked_imaging_7x7, +): + import autoarray as aa + + model, analysis = _shared_mesh_analysis(masked_imaging_7x7, shared_preloads=True) + instance = model.instance_from_unit_vector([]) + + # `shared_state_from` builds a `PreloadsImaging` carrying the source-plane mesh geometry (the + # exposure-invariant quantity) — NOT the mapper / curvature matrix / regularization matrix, + # which are per-exposure for imaging (PSFs, offsets, adaptive regularization). + shared = analysis.shared_state_from(instance=instance) + assert isinstance(shared, aa.PreloadsImaging) + assert shared.source_plane_mesh_grid is not None + assert shared.image_plane_mesh_grid is not None + assert shared.curvature_matrix is None + assert shared.mapper_galaxy_dict is None + + # The preloaded mesh is reused by the fit (identity) and leaves the figure of merit unchanged. + fit_unshared = analysis.fit_from(instance=instance) + fom_unshared = fit_unshared.figure_of_merit + + fit_shared = analysis.fit_from(instance=instance, preloads=shared) + + assert ( + fit_shared.tracer_to_inversion.traced_mesh_grid_pg_list + is shared.source_plane_mesh_grid + ) + assert fit_shared.figure_of_merit == pytest.approx(fom_unshared) + + # The full `log_likelihood_function` with the shared object matches the unshared call. + assert analysis.log_likelihood_function( + instance=instance, shared=shared + ) == pytest.approx(analysis.log_likelihood_function(instance=instance)) + + +def test__shared_state_from__returns_none_when_not_opted_in(masked_imaging_7x7): + model, analysis = _shared_mesh_analysis(masked_imaging_7x7, shared_preloads=False) + instance = model.instance_from_unit_vector([]) + + assert analysis.shared_state_from(instance=instance) is None + + +def test__shared_state_from__returns_none_when_no_inversion(masked_imaging_7x7): + lens = al.Galaxy(redshift=0.5, light=al.lp.Sersic(intensity=0.1)) + + model = af.Collection(galaxies=af.Collection(lens=lens)) + instance = model.instance_from_unit_vector([]) + + analysis = al.AnalysisImaging( + dataset=masked_imaging_7x7, use_jax=False, shared_preloads=True + ) + + assert analysis.shared_state_from(instance=instance) is None + + +def _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads): + factors = [] + model = None + for _ in range(2): + model, analysis = _shared_mesh_analysis(masked_imaging_7x7, shared_preloads) + factors.append(af.AnalysisFactor(prior_model=model.copy(), analysis=analysis)) + + factor_graph = af.FactorGraphModel(*factors) + instance = factor_graph.global_prior_model.instance_from_unit_vector([]) + return factor_graph.log_likelihood_function(instance) + + +def test__factor_graph__shared_vs_unshared_equal(masked_imaging_7x7): + ll_unshared = _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads=False) + ll_shared = _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads=True) + + print(f"\nunshared={ll_unshared} shared={ll_shared}") + assert ll_shared == pytest.approx(ll_unshared, rel=1e-10) + + +def test__factor_graph__shared_state_computed_once(masked_imaging_7x7, monkeypatch): + calls = {"n": 0} + + original = al.AnalysisImaging.shared_state_from + + def counting(self, instance): + result = original(self, instance) + if result is not None: + calls["n"] += 1 + return result + + monkeypatch.setattr(al.AnalysisImaging, "shared_state_from", counting) + + _factor_graph_log_likelihood(masked_imaging_7x7, shared_preloads=True) + + assert calls["n"] == 1 + + +def test__preloads_scoped__cross_type_preloads_reduced_to_mesh_view(masked_imaging_7x7): + import autoarray as aa + + lens = al.Galaxy(redshift=0.5, light=al.lp.Sersic(intensity=0.1)) + tracer = al.Tracer(galaxies=[lens]) + + # Cross-dataset-type preloads (e.g. from an interferometer lead factor in a joint graph): + # the mapper / curvature matrix embed the other dataset's grids and must NOT be consumed + # by an imaging fit — only the mesh-geometry view survives the scoping. + cross_type = aa.PreloadsInterferometer( + curvature_matrix="other-datasets-F", + mapper_galaxy_dict="other-datasets-mapper", + source_plane_mesh_grid=[["mesh"]], + image_plane_mesh_grid=[["image-mesh"]], + ) + + fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer, preloads=cross_type) + + scoped = fit._preloads_scoped + assert isinstance(scoped, aa.PreloadsImaging) + assert scoped.source_plane_mesh_grid == [["mesh"]] + assert scoped.image_plane_mesh_grid == [["image-mesh"]] + assert scoped.curvature_matrix is None + assert scoped.mapper_galaxy_dict is None + + # Same-type preloads pass through untouched. + same_type = aa.PreloadsImaging(source_plane_mesh_grid=[["mesh"]]) + fit = al.FitImaging(dataset=masked_imaging_7x7, tracer=tracer, preloads=same_type) + assert fit._preloads_scoped is same_type diff --git a/test_autolens/imaging/model/test_result_imaging.py b/test_autolens/imaging/model/test_result_imaging.py index 0cad3ae94..b832b593e 100644 --- a/test_autolens/imaging/model/test_result_imaging.py +++ b/test_autolens/imaging/model/test_result_imaging.py @@ -1,27 +1,27 @@ -import pytest - -import autofit as af -import autolens as al - -from autolens.imaging.model.result import ResultImaging - - -def test___linear_light_profiles_in_result(analysis_imaging_7x7): - - galaxies = af.ModelInstance() - galaxies.galaxy = al.Galaxy(redshift=0.5, bulge=al.lp_linear.Sersic(centre=(0.05, 0.05))) - - instance = af.ModelInstance() - instance.galaxies = galaxies - - samples_summary = al.m.MockSamplesSummary(max_log_likelihood_instance=instance) - - result = ResultImaging(samples_summary=samples_summary, analysis=analysis_imaging_7x7) - - assert not isinstance( - result.max_log_likelihood_tracer.galaxies[0].bulge, - al.lp_linear.LightProfileLinear, - ) - assert result.max_log_likelihood_tracer.galaxies[ - 0 - ].bulge.intensity == pytest.approx(0.1868684644, 1.0e-4) +import pytest + +import autofit as af +import autolens as al + +from autolens.imaging.model.result import ResultImaging + + +def test___linear_light_profiles_in_result(analysis_imaging_7x7): + + galaxies = af.ModelInstance() + galaxies.galaxy = al.Galaxy(redshift=0.5, bulge=al.lp_linear.Sersic(centre=(0.05, 0.05))) + + instance = af.ModelInstance() + instance.galaxies = galaxies + + samples_summary = al.m.MockSamplesSummary(max_log_likelihood_instance=instance) + + result = ResultImaging(samples_summary=samples_summary, analysis=analysis_imaging_7x7) + + assert not isinstance( + result.max_log_likelihood_tracer.galaxies[0].bulge, + al.lp_linear.LightProfileLinear, + ) + assert result.max_log_likelihood_tracer.galaxies[ + 0 + ].bulge.intensity == pytest.approx(0.1868684644, 1.0e-4) diff --git a/test_autolens/imaging/test_simulate_and_fit_imaging.py b/test_autolens/imaging/test_simulate_and_fit_imaging.py index f8232fad3..7c5e88eba 100644 --- a/test_autolens/imaging/test_simulate_and_fit_imaging.py +++ b/test_autolens/imaging/test_simulate_and_fit_imaging.py @@ -1,927 +1,927 @@ -import os -from pathlib import Path -import shutil - -import autolens as al -import numpy as np -import pytest - - -def test__perfect_fit__chi_squared_0(): - - grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=1) - - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy = al.Galaxy( - redshift=1.0, light=al.lp.Exponential(centre=(0.1, 0.1), intensity=0.5) - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) - - dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) - dataset.noise_map = al.Array2D.ones( - shape_native=dataset.data.shape_native, pixel_scales=0.2 - ) - - file_path = Path(__file__).resolve().parent / "data_temp" / "simulate_and_fit" - - try: - shutil.rmtree(file_path) - except FileNotFoundError: - pass - - if not file_path.exists(): - os.makedirs(file_path) - - from autoarray.dataset.plot.imaging_plots import fits_imaging - - fits_imaging( - dataset=dataset, - data_path=file_path / "data.fits", - noise_map_path=file_path / "noise_map.fits", - psf_path=file_path / "psf.fits", - overwrite=True, - ) - - dataset = al.Imaging.from_fits( - data_path=file_path / "data.fits", - noise_map_path=file_path / "noise_map.fits", - psf_path=file_path / "psf.fits", - pixel_scales=0.2, - over_sample_size_lp=1 - ) - - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.8 - ) - - masked_dataset = dataset.apply_mask(mask=mask) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) - - assert fit.chi_squared == pytest.approx(0.0, 1e-4) - - file_path = Path(__file__).resolve().parent / "data_temp" - - if file_path.exists(): - shutil.rmtree(file_path) - - -def _perfect_lens_fit_dataset(tracer, grid): - """Helper: simulate noiseless imaging through a tracer and unit noise map.""" - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=grid.pixel_scales[0], sigma=0.05, normalize=True - ) - simulator = al.SimulatorImaging( - exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False - ) - dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) - dataset.noise_map = al.Array2D.ones( - shape_native=dataset.data.shape_native, pixel_scales=grid.pixel_scales - ) - return dataset - - -def test__perfect_fit__sim_offset_lens_and_source__fit_with_dataset_model_grid_offset__chi_squared_zero(): - """Sim a lens system shifted away from origin; fit with origin-centred profiles + - DatasetModel.grid_offset.""" - grid = al.Grid2D.uniform(shape_native=(31, 31), pixel_scales=0.2, over_sample_size=1) - offset = (0.3, 0.2) - - lens_sim = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=offset, intensity=0.1, effective_radius=0.3), - mass=al.mp.Isothermal(centre=offset, einstein_radius=1.0), - ) - src_sim = al.Galaxy( - redshift=1.0, - light=al.lp.Sersic( - centre=(offset[0] + 0.05, offset[1] + 0.05), - intensity=0.5, - effective_radius=0.3, - ), - ) - tracer_sim = al.Tracer(galaxies=[lens_sim, src_sim]) - dataset = _perfect_lens_fit_dataset(tracer_sim, grid) - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=2.5 - ) - masked = dataset.apply_mask(mask=mask) - - lens_fit = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.0, 0.0), intensity=0.1, effective_radius=0.3), - mass=al.mp.Isothermal(centre=(0.0, 0.0), einstein_radius=1.0), - ) - src_fit = al.Galaxy( - redshift=1.0, - light=al.lp.Sersic( - centre=(0.05, 0.05), intensity=0.5, effective_radius=0.3 - ), - ) - tracer_fit = al.Tracer(galaxies=[lens_fit, src_fit]) - dataset_model = al.DatasetModel(grid_offset=offset) - fit = al.FitImaging( - dataset=masked, tracer=tracer_fit, dataset_model=dataset_model - ) - - assert fit.chi_squared == pytest.approx(0.0, abs=1e-4) - - -def test__perfect_fit__sim_rotated_lens_mass__fit_with_dataset_model_grid_rotation__chi_squared_zero(): - """Sim a strong lens with a rotated mass ellipse; fit with axis-aligned mass + - DatasetModel.grid_rotation_angle. The source centre is pre-rotated by -theta about - the origin to compensate for the grid rotation.""" - import numpy as np - - grid = al.Grid2D.uniform(shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1) - theta = 10.0 - src_centre = (0.05, 0.05) - - lens_sim = al.Galaxy( - redshift=0.5, - mass=al.mp.Isothermal( - centre=(0.0, 0.0), - einstein_radius=1.2, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=theta), - ), - ) - src_sim = al.Galaxy( - redshift=1.0, - light=al.lp.SersicCore( - centre=src_centre, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=theta + 30.0), - intensity=0.5, - effective_radius=0.1, - ), - ) - tracer_sim = al.Tracer(galaxies=[lens_sim, src_sim]) - dataset = _perfect_lens_fit_dataset(tracer_sim, grid) - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.1, radius=2.0 - ) - masked = dataset.apply_mask(mask=mask) - - # Rotate the source centre by -theta about the origin (compensating for the - # grid rotation by +theta). - cos_t = np.cos(-np.deg2rad(theta)) - sin_t = np.sin(-np.deg2rad(theta)) - src_centre_rotated = ( - src_centre[1] * sin_t + src_centre[0] * cos_t, - src_centre[1] * cos_t - src_centre[0] * sin_t, - ) - - lens_fit = al.Galaxy( - redshift=0.5, - mass=al.mp.Isothermal( - centre=(0.0, 0.0), - einstein_radius=1.2, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=0.0), - ), - ) - src_fit = al.Galaxy( - redshift=1.0, - light=al.lp.SersicCore( - centre=src_centre_rotated, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=30.0), - intensity=0.5, - effective_radius=0.1, - ), - ) - tracer_fit = al.Tracer(galaxies=[lens_fit, src_fit]) - dataset_model = al.DatasetModel(grid_rotation_angle=-theta) - fit = al.FitImaging( - dataset=masked, tracer=tracer_fit, dataset_model=dataset_model - ) - - assert fit.chi_squared == pytest.approx(0.0, abs=1e-4) - - -def test__perfect_fit__sim_offset_and_rotated_lens__fit_with_dataset_model_offset_and_rotation__chi_squared_zero(): - """Combined offset + rotation for the strong-lens use case relevant to Hannah's - multi-band JWST fits: sim with shifted-and-rotated lens, fit at identity profiles + - DatasetModel carrying both transforms.""" - import numpy as np - - grid = al.Grid2D.uniform(shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1) - offset = (0.2, 0.1) - theta = 8.0 - - # In the simulated data frame: profiles have centres at (cy+oy, cx+ox) where - # the (cy, cx) is the "model frame" centre rotated by +theta about the offset. - cos_t = np.cos(np.deg2rad(theta)) - sin_t = np.sin(np.deg2rad(theta)) - - def to_sim_frame(model_centre): - # Rotate by +theta about origin, then add offset. - y_rot = model_centre[1] * sin_t + model_centre[0] * cos_t - x_rot = model_centre[1] * cos_t - model_centre[0] * sin_t - return (y_rot + offset[0], x_rot + offset[1]) - - model_lens_centre = (0.0, 0.0) - model_src_centre = (0.05, 0.05) - - lens_sim = al.Galaxy( - redshift=0.5, - mass=al.mp.Isothermal( - centre=to_sim_frame(model_lens_centre), - einstein_radius=1.0, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=theta), - ), - ) - src_sim = al.Galaxy( - redshift=1.0, - light=al.lp.SersicCore( - centre=to_sim_frame(model_src_centre), - ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=theta + 20.0), - intensity=0.4, - effective_radius=0.1, - ), - ) - tracer_sim = al.Tracer(galaxies=[lens_sim, src_sim]) - dataset = _perfect_lens_fit_dataset(tracer_sim, grid) - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.1, radius=2.0 - ) - masked = dataset.apply_mask(mask=mask) - - lens_fit = al.Galaxy( - redshift=0.5, - mass=al.mp.Isothermal( - centre=model_lens_centre, - einstein_radius=1.0, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=0.0), - ), - ) - src_fit = al.Galaxy( - redshift=1.0, - light=al.lp.SersicCore( - centre=model_src_centre, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=20.0), - intensity=0.4, - effective_radius=0.1, - ), - ) - tracer_fit = al.Tracer(galaxies=[lens_fit, src_fit]) - dataset_model = al.DatasetModel(grid_offset=offset, grid_rotation_angle=-theta) - fit = al.FitImaging( - dataset=masked, tracer=tracer_fit, dataset_model=dataset_model - ) - - assert fit.chi_squared == pytest.approx(0.0, abs=1e-4) - - -def test__simulate_imaging_data_and_fit__known_likelihood(): - - grid = al.Grid2D.uniform(shape_native=(31, 31), pixel_scales=0.2) - - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True - ) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(16, 16)), - regularization=al.reg.Constant(coefficient=(1.0)), - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - bulge=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), - disk=al.lp.Sersic(centre=(0.2, 0.2), intensity=0.2), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) - source_galaxy_1 = al.Galaxy(redshift=2.0, pixelization=pixelization) - tracer = al.Tracer( - galaxies=[lens_galaxy, source_galaxy_0, source_galaxy_1] - ) - - simulator = al.SimulatorImaging(exposure_time=300.0, psf=psf, noise_seed=1) - - dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) - - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=2.005 - ) - - masked_dataset = dataset.apply_mask(mask=mask) - - fit = al.FitImaging(dataset=masked_dataset, tracer=tracer, - settings=al.Settings(use_border_relocator=True) - ) - - assert fit.figure_of_merit == pytest.approx(565.6348654, 1.0e-2) - - -def test__simulate_imaging_data_and_fit__linear_light_profiles_agree_with_standard_light_profiles(): - - grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=1) - - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy = al.Galaxy( - redshift=1.0, - bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), - disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) - - dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) - dataset.noise_map = al.Array2D.ones( - shape_native=dataset.data.shape_native, pixel_scales=0.2 - ) - - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 - ) - - masked_dataset = dataset.apply_mask(mask=mask) - masked_dataset = masked_dataset.apply_over_sampling( - over_sample_size_lp=1 - ) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) - - lens_galaxy_linear = al.Galaxy( - redshift=0.5, - light=al.lp_linear.Sersic(centre=(0.1, 0.1)), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy_linear = al.Galaxy( - redshift=1.0, - bulge=al.lp_linear.Sersic(sersic_index=1.0), - disk=al.lp_linear.Sersic(sersic_index=4.0), - ) - - tracer_linear = al.Tracer( - galaxies=[lens_galaxy_linear, source_galaxy_linear] - ) - - fit_linear = al.FitImaging( - dataset=masked_dataset, - tracer=tracer_linear, - ) - - assert fit_linear.inversion.reconstruction == pytest.approx( - np.array([0.1, 0.1, 0.2]), 1.0e-4 - ) - assert fit_linear.linear_light_profile_intensity_dict[ - lens_galaxy_linear.light - ] == pytest.approx(0.1, 1.0e-2) - assert fit_linear.linear_light_profile_intensity_dict[ - source_galaxy_linear.bulge - ] == pytest.approx(0.1, 1.0e-2) - assert fit_linear.linear_light_profile_intensity_dict[ - source_galaxy_linear.disk - ] == pytest.approx(0.2, 1.0e-2) - assert fit.log_likelihood == fit_linear.figure_of_merit - assert fit_linear.figure_of_merit == pytest.approx(-45.02798, 1.0e-4) - - lens_galaxy_image = lens_galaxy.blurred_image_2d_from( - grid=masked_dataset.grids.lp, - psf=masked_dataset.psf, - blurring_grid=masked_dataset.grids.blurring, - ) - - assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( - lens_galaxy_image.array, 1.0e-4 - ) - assert fit_linear.model_images_of_planes_list[0] == pytest.approx( - lens_galaxy_image.array, 1.0e-4 - ) - - traced_grid_2d_list = tracer.traced_grid_2d_list_from(grid=masked_dataset.grids.lp) - traced_blurring_grid_2d_list = tracer.traced_grid_2d_list_from( - grid=masked_dataset.grids.blurring - ) - - source_galaxy_image = source_galaxy.blurred_image_2d_from( - grid=traced_grid_2d_list[1], - psf=masked_dataset.psf, - blurring_grid=traced_blurring_grid_2d_list[1], - ) - - assert fit_linear.galaxy_model_image_dict[source_galaxy_linear] == pytest.approx( - source_galaxy_image.array, 1.0e-4 - ) - - assert fit_linear.model_images_of_planes_list[1] == pytest.approx( - source_galaxy_image.array, 1.0e-4 - ) - - -def test__simulate_imaging_data_and_fit__linear_light_profiles_and_pixelization(): - - grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=1) - - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy = al.Galaxy( - redshift=1.0, - bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), - disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) - - dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) - dataset.noise_map = al.Array2D.ones( - shape_native=dataset.data.shape_native, pixel_scales=0.2 - ) - - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 - ) - - masked_dataset = dataset.apply_mask(mask=mask) - masked_dataset = masked_dataset.apply_over_sampling( - over_sample_size_lp=1 - ) - - lens_galaxy_linear = al.Galaxy( - redshift=0.5, - light=al.lp_linear.Sersic(centre=(0.1, 0.1)), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=0.01), - ) - - source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer_linear = al.Tracer( - galaxies=[lens_galaxy_linear, source_galaxy_pix] - ) - - fit_linear = al.FitImaging( - dataset=masked_dataset, - tracer=tracer_linear, - settings=al.Settings(use_border_relocator=True) - ) - - assert fit_linear.inversion.reconstruction[0:2] == pytest.approx( - np.array( - [ - 99.993449641, 0.114213814, - ] - ), - 1.0e-4, - ) - assert fit_linear.figure_of_merit == pytest.approx(-87.6933733814, 1.0e-4) - - lens_galaxy_image = lens_galaxy.blurred_image_2d_from( - grid=masked_dataset.grids.lp, - psf=masked_dataset.psf, - blurring_grid=masked_dataset.grids.blurring, - ) - - assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( - lens_galaxy_image.array, 1.0e-2 - ) - assert fit_linear.model_images_of_planes_list[0] == pytest.approx( - lens_galaxy_image.array, 1.0e-2 - ) - - assert fit_linear.galaxy_model_image_dict[source_galaxy_pix][0] == pytest.approx( - 0.0524952137, 1.0e-4 - ) - - assert fit_linear.model_images_of_planes_list[1][0] == pytest.approx( - 0.052495213, 1.0e-4 - ) - - fit_linear = al.FitImaging( - dataset=masked_dataset, - tracer=tracer_linear, - settings=al.Settings( - use_positive_only_solver=True, - ), - ) - - assert fit_linear.inversion.reconstruction == pytest.approx( - np.array( - [ - 100.01548, - 0.0, - 0.0, - 0.0, - 0.0, - 1.13328604, - 0.0, - 0.0, - 0.0, - 0.0, - ] - ), - abs=1.0e-1, - ) - assert fit_linear.figure_of_merit == pytest.approx(-85.7890126577, 1.0e-4) - - -def test__simulate_imaging_data_and_fit__linear_light_profiles_and_pixelization__sub_2(): - - grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=2) - - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy = al.Galaxy( - redshift=1.0, - bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), - disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) - - dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) - dataset.noise_map = al.Array2D.ones( - shape_native=dataset.data.shape_native, pixel_scales=0.2 - ) - - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 - ) - - dataset = al.Imaging( - data=dataset.data, - psf=dataset.psf, - noise_map=dataset.noise_map, - over_sample_size_lp=2, - over_sample_size_pixelization=2 - ) - - masked_dataset = dataset.apply_mask(mask=mask) - - lens_galaxy_linear = al.Galaxy( - redshift=0.5, - light=al.lp_linear.Sersic(centre=(0.1, 0.1)), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=0.01), - ) - - source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer_linear = al.Tracer( - galaxies=[lens_galaxy_linear, source_galaxy_pix] - ) - - fit_linear = al.FitImaging( - dataset=masked_dataset, - tracer=tracer_linear, - settings=al.Settings(use_border_relocator=True) - ) - - assert fit_linear.inversion.reconstruction[0:2] == pytest.approx( - np.array( - [ - 99.974078996, 0.251635768, - ] - ), - 1.0e-4, - ) - - assert fit_linear.figure_of_merit == pytest.approx(-87.9411586204183, 1.0e-4) - - lens_galaxy_image = lens_galaxy.blurred_image_2d_from( - grid=masked_dataset.grids.lp, - psf=masked_dataset.psf, - blurring_grid=masked_dataset.grids.blurring, - ) - - assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( - lens_galaxy_image.array, 1.0e-2 - ) - assert fit_linear.model_images_of_planes_list[0] == pytest.approx( - lens_galaxy_image.array, 1.0e-2 - ) - - assert fit_linear.galaxy_model_image_dict[source_galaxy_pix][0] == pytest.approx( - 0.134961296372, 1.0e-4 - ) - - assert fit_linear.model_images_of_planes_list[1][0] == pytest.approx( - 0.134961296, 1.0e-4 - ) - - assert fit_linear.subtracted_images_of_planes_list[1][0] == pytest.approx( - 0.34355239059, 1.0e-4 - ) - - fit_linear = al.FitImaging( - dataset=masked_dataset, - tracer=tracer_linear, - settings=al.Settings( - use_positive_only_solver=True, - ), - ) - - assert fit_linear.inversion.reconstruction == pytest.approx( - np.array( - [ - 100.01548, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ] - ), - abs=1.0e-1, - ) - assert fit_linear.figure_of_merit == pytest.approx(-86.95872610, abs=1.0e-4) - - -def test__simulate_imaging_data_and_fit__linear_light_profiles_and_pixelization__delaunay_split(): - - grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=2) - - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy = al.Galaxy( - redshift=1.0, - bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), - disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) - - dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) - dataset.noise_map = al.Array2D.ones( - shape_native=dataset.data.shape_native, pixel_scales=0.2 - ) - - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.81 - ) - - dataset = al.Imaging( - data=dataset.data, - psf=dataset.psf, - noise_map=dataset.noise_map, - over_sample_size_lp=2, - over_sample_size_pixelization=2 - ) - - masked_dataset = dataset.apply_mask(mask=mask) - - lens_galaxy_linear = al.Galaxy( - redshift=0.5, - light=al.lp_linear.Sersic(centre=(0.1, 0.1)), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - - pixelization = al.Pixelization( - mesh=al.mesh.Delaunay(pixels=25, zeroed_pixels=5), - regularization=al.reg.AdaptSplit(inner_coefficient=0.01, outer_coefficient=0.1, signal_scale=0.1), - ) - - source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) - - image_mesh = al.image_mesh.Overlay(shape=(7, 7)) - - image_plane_mesh_grid = image_mesh.image_plane_mesh_grid_from( - mask=masked_dataset.mask, - ) - - adapt_images = al.AdaptImages( - galaxy_image_dict={source_galaxy_pix: masked_dataset.data}, - galaxy_image_plane_mesh_grid_dict={source_galaxy_pix: image_plane_mesh_grid}, - ) - - tracer_linear = al.Tracer( - galaxies=[lens_galaxy_linear, source_galaxy_pix] - ) - - fit_linear = al.FitImaging( - dataset=masked_dataset, - tracer=tracer_linear, - adapt_images=adapt_images, - settings=al.Settings(use_positive_only_solver=False), - ) - - assert fit_linear.inversion.reconstruction[0:3] == pytest.approx( - np.array( - [ - 1.00179579e+02, 5.35321466e-01, 8.55754143e-01 - ] - ), - 1.0e-4, - ) - - assert fit_linear.figure_of_merit == pytest.approx(-190.564548990939, 1.0e-4) - - lens_galaxy_image = lens_galaxy.blurred_image_2d_from( - grid=masked_dataset.grids.lp, - blurring_grid=masked_dataset.grids.blurring, - psf=masked_dataset.psf - ) - - assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( - lens_galaxy_image, 1.0e-2 - ) - assert fit_linear.model_images_of_planes_list[0] == pytest.approx( - lens_galaxy_image, 1.0e-2 - ) - - assert fit_linear.galaxy_model_image_dict[source_galaxy_pix][0] == pytest.approx( - 0.1667703826, 1.0e-4 - ) - - assert fit_linear.model_images_of_planes_list[1][0] == pytest.approx( - 0.166757208736973, 1.0e-4 - ) - - assert fit_linear.subtracted_images_of_planes_list[1][0] == pytest.approx( - 0.180018267146, 1.0e-4 - ) - - - fit_linear = al.FitImaging( - dataset=masked_dataset, - tracer=tracer_linear, - adapt_images=adapt_images, - settings=al.Settings( - use_positive_only_solver=True, - use_edge_zeroed_pixels=True - ), - ) - - assert fit_linear.inversion.reconstruction[0:2] == pytest.approx( - np.array( - [ - 99.9785287998059, - 0.8958653625423 - ] - ), - 1.0e-4, - ) - assert fit_linear.figure_of_merit == pytest.approx(-190.6935526756, 1.0e-4) - - -def test__fit_figure_of_merit__mge_mass_model(masked_imaging_7x7, masked_imaging_covariance_7x7): - - grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, - over_sample_size=8) - - psf = al.Convolver.from_gaussian( - shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light_0=al.lp.Gaussian(intensity=1.0), - light_1=al.lp.Gaussian(intensity=2.0), - mass_0=al.mp.Gaussian(intensity=1.0, mass_to_light_ratio=3.0), - mass_1=al.mp.Gaussian(intensity=2.0, mass_to_light_ratio=4.0), - ) - source_galaxy = al.Galaxy( - redshift=1.0, light=al.lp.Exponential(centre=(0.1, 0.1), intensity=0.5) - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) - - dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) - dataset.noise_map = al.Array2D.ones( - shape_native=dataset.data.shape_native, pixel_scales=0.2 - ) - - file_path = Path(__file__).resolve().parent / "data_temp" / "simulate_and_fit" - - try: - shutil.rmtree(file_path) - except FileNotFoundError: - pass - - if not file_path.exists(): - os.makedirs(file_path) - - from autoarray.dataset.plot.imaging_plots import fits_imaging - - fits_imaging( - dataset=dataset, - data_path=file_path / "data.fits", - noise_map_path=file_path / "noise_map.fits", - psf_path=file_path / "psf.fits", - overwrite=True, - ) - - dataset = al.Imaging.from_fits( - data_path=file_path / "data.fits", - noise_map_path=file_path / "noise_map.fits", - psf_path=file_path / "psf.fits", - pixel_scales=0.2, - over_sample_size_lp=8 - ) - - mask = al.Mask2D.circular( - shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 - ) - - masked_dataset = dataset.apply_mask(mask=mask) - - basis = al.lp_basis.Basis( - profile_list=[ - al.lmp.Gaussian(intensity=1.0, mass_to_light_ratio=3.0), - al.lmp.Gaussian(intensity=2.0, mass_to_light_ratio=4.0), - ] - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - bulge=basis, - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) - - # The value is actually not zero before the blurring grid assumes a sub_size=1 - # and does not use the iterative grid, which has a small impact on the chi-squared - - assert fit.chi_squared == pytest.approx(5.706423629698664e-05, 1e-4) - - masked_dataset = masked_dataset.apply_over_sampling( - over_sample_size_lp=8 - ) - - basis = al.lp_basis.Basis( - profile_list=[ - al.lmp_linear.Gaussian(intensity=1.0, mass_to_light_ratio=3.0), - al.lmp_linear.Gaussian(intensity=2.0, mass_to_light_ratio=4.0), - ] - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - bulge=basis, - ) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) - - # The value is actually not zero before the blurring grid assumes a sub_size=1 - # and does not use the iterative grid, which has a small impact on the chi-squared - - assert fit.chi_squared == pytest.approx(3.295535243634485e-05, 1e-4) - - file_path = Path(__file__).resolve().parent / "data_temp" - - if file_path.exists(): +import os +from pathlib import Path +import shutil + +import autolens as al +import numpy as np +import pytest + + +def test__perfect_fit__chi_squared_0(): + + grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=1) + + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy = al.Galaxy( + redshift=1.0, light=al.lp.Exponential(centre=(0.1, 0.1), intensity=0.5) + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) + + dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) + dataset.noise_map = al.Array2D.ones( + shape_native=dataset.data.shape_native, pixel_scales=0.2 + ) + + file_path = Path(__file__).resolve().parent / "data_temp" / "simulate_and_fit" + + try: + shutil.rmtree(file_path) + except FileNotFoundError: + pass + + if not file_path.exists(): + os.makedirs(file_path) + + from autoarray.dataset.plot.imaging_plots import fits_imaging + + fits_imaging( + dataset=dataset, + data_path=file_path / "data.fits", + noise_map_path=file_path / "noise_map.fits", + psf_path=file_path / "psf.fits", + overwrite=True, + ) + + dataset = al.Imaging.from_fits( + data_path=file_path / "data.fits", + noise_map_path=file_path / "noise_map.fits", + psf_path=file_path / "psf.fits", + pixel_scales=0.2, + over_sample_size_lp=1 + ) + + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.8 + ) + + masked_dataset = dataset.apply_mask(mask=mask) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) + + assert fit.chi_squared == pytest.approx(0.0, 1e-4) + + file_path = Path(__file__).resolve().parent / "data_temp" + + if file_path.exists(): + shutil.rmtree(file_path) + + +def _perfect_lens_fit_dataset(tracer, grid): + """Helper: simulate noiseless imaging through a tracer and unit noise map.""" + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=grid.pixel_scales[0], sigma=0.05, normalize=True + ) + simulator = al.SimulatorImaging( + exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False + ) + dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) + dataset.noise_map = al.Array2D.ones( + shape_native=dataset.data.shape_native, pixel_scales=grid.pixel_scales + ) + return dataset + + +def test__perfect_fit__sim_offset_lens_and_source__fit_with_dataset_model_grid_offset__chi_squared_zero(): + """Sim a lens system shifted away from origin; fit with origin-centred profiles + + DatasetModel.grid_offset.""" + grid = al.Grid2D.uniform(shape_native=(31, 31), pixel_scales=0.2, over_sample_size=1) + offset = (0.3, 0.2) + + lens_sim = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=offset, intensity=0.1, effective_radius=0.3), + mass=al.mp.Isothermal(centre=offset, einstein_radius=1.0), + ) + src_sim = al.Galaxy( + redshift=1.0, + light=al.lp.Sersic( + centre=(offset[0] + 0.05, offset[1] + 0.05), + intensity=0.5, + effective_radius=0.3, + ), + ) + tracer_sim = al.Tracer(galaxies=[lens_sim, src_sim]) + dataset = _perfect_lens_fit_dataset(tracer_sim, grid) + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=2.5 + ) + masked = dataset.apply_mask(mask=mask) + + lens_fit = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.0, 0.0), intensity=0.1, effective_radius=0.3), + mass=al.mp.Isothermal(centre=(0.0, 0.0), einstein_radius=1.0), + ) + src_fit = al.Galaxy( + redshift=1.0, + light=al.lp.Sersic( + centre=(0.05, 0.05), intensity=0.5, effective_radius=0.3 + ), + ) + tracer_fit = al.Tracer(galaxies=[lens_fit, src_fit]) + dataset_model = al.DatasetModel(grid_offset=offset) + fit = al.FitImaging( + dataset=masked, tracer=tracer_fit, dataset_model=dataset_model + ) + + assert fit.chi_squared == pytest.approx(0.0, abs=1e-4) + + +def test__perfect_fit__sim_rotated_lens_mass__fit_with_dataset_model_grid_rotation__chi_squared_zero(): + """Sim a strong lens with a rotated mass ellipse; fit with axis-aligned mass + + DatasetModel.grid_rotation_angle. The source centre is pre-rotated by -theta about + the origin to compensate for the grid rotation.""" + import numpy as np + + grid = al.Grid2D.uniform(shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1) + theta = 10.0 + src_centre = (0.05, 0.05) + + lens_sim = al.Galaxy( + redshift=0.5, + mass=al.mp.Isothermal( + centre=(0.0, 0.0), + einstein_radius=1.2, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=theta), + ), + ) + src_sim = al.Galaxy( + redshift=1.0, + light=al.lp.SersicCore( + centre=src_centre, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=theta + 30.0), + intensity=0.5, + effective_radius=0.1, + ), + ) + tracer_sim = al.Tracer(galaxies=[lens_sim, src_sim]) + dataset = _perfect_lens_fit_dataset(tracer_sim, grid) + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.1, radius=2.0 + ) + masked = dataset.apply_mask(mask=mask) + + # Rotate the source centre by -theta about the origin (compensating for the + # grid rotation by +theta). + cos_t = np.cos(-np.deg2rad(theta)) + sin_t = np.sin(-np.deg2rad(theta)) + src_centre_rotated = ( + src_centre[1] * sin_t + src_centre[0] * cos_t, + src_centre[1] * cos_t - src_centre[0] * sin_t, + ) + + lens_fit = al.Galaxy( + redshift=0.5, + mass=al.mp.Isothermal( + centre=(0.0, 0.0), + einstein_radius=1.2, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=0.0), + ), + ) + src_fit = al.Galaxy( + redshift=1.0, + light=al.lp.SersicCore( + centre=src_centre_rotated, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=30.0), + intensity=0.5, + effective_radius=0.1, + ), + ) + tracer_fit = al.Tracer(galaxies=[lens_fit, src_fit]) + dataset_model = al.DatasetModel(grid_rotation_angle=-theta) + fit = al.FitImaging( + dataset=masked, tracer=tracer_fit, dataset_model=dataset_model + ) + + assert fit.chi_squared == pytest.approx(0.0, abs=1e-4) + + +def test__perfect_fit__sim_offset_and_rotated_lens__fit_with_dataset_model_offset_and_rotation__chi_squared_zero(): + """Combined offset + rotation for the strong-lens use case relevant to Hannah's + multi-band JWST fits: sim with shifted-and-rotated lens, fit at identity profiles + + DatasetModel carrying both transforms.""" + import numpy as np + + grid = al.Grid2D.uniform(shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1) + offset = (0.2, 0.1) + theta = 8.0 + + # In the simulated data frame: profiles have centres at (cy+oy, cx+ox) where + # the (cy, cx) is the "model frame" centre rotated by +theta about the offset. + cos_t = np.cos(np.deg2rad(theta)) + sin_t = np.sin(np.deg2rad(theta)) + + def to_sim_frame(model_centre): + # Rotate by +theta about origin, then add offset. + y_rot = model_centre[1] * sin_t + model_centre[0] * cos_t + x_rot = model_centre[1] * cos_t - model_centre[0] * sin_t + return (y_rot + offset[0], x_rot + offset[1]) + + model_lens_centre = (0.0, 0.0) + model_src_centre = (0.05, 0.05) + + lens_sim = al.Galaxy( + redshift=0.5, + mass=al.mp.Isothermal( + centre=to_sim_frame(model_lens_centre), + einstein_radius=1.0, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=theta), + ), + ) + src_sim = al.Galaxy( + redshift=1.0, + light=al.lp.SersicCore( + centre=to_sim_frame(model_src_centre), + ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=theta + 20.0), + intensity=0.4, + effective_radius=0.1, + ), + ) + tracer_sim = al.Tracer(galaxies=[lens_sim, src_sim]) + dataset = _perfect_lens_fit_dataset(tracer_sim, grid) + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.1, radius=2.0 + ) + masked = dataset.apply_mask(mask=mask) + + lens_fit = al.Galaxy( + redshift=0.5, + mass=al.mp.Isothermal( + centre=model_lens_centre, + einstein_radius=1.0, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.7, angle=0.0), + ), + ) + src_fit = al.Galaxy( + redshift=1.0, + light=al.lp.SersicCore( + centre=model_src_centre, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=20.0), + intensity=0.4, + effective_radius=0.1, + ), + ) + tracer_fit = al.Tracer(galaxies=[lens_fit, src_fit]) + dataset_model = al.DatasetModel(grid_offset=offset, grid_rotation_angle=-theta) + fit = al.FitImaging( + dataset=masked, tracer=tracer_fit, dataset_model=dataset_model + ) + + assert fit.chi_squared == pytest.approx(0.0, abs=1e-4) + + +def test__simulate_imaging_data_and_fit__known_likelihood(): + + grid = al.Grid2D.uniform(shape_native=(31, 31), pixel_scales=0.2) + + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True + ) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(16, 16)), + regularization=al.reg.Constant(coefficient=(1.0)), + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + bulge=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), + disk=al.lp.Sersic(centre=(0.2, 0.2), intensity=0.2), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) + source_galaxy_1 = al.Galaxy(redshift=2.0, pixelization=pixelization) + tracer = al.Tracer( + galaxies=[lens_galaxy, source_galaxy_0, source_galaxy_1] + ) + + simulator = al.SimulatorImaging(exposure_time=300.0, psf=psf, noise_seed=1) + + dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) + + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=2.005 + ) + + masked_dataset = dataset.apply_mask(mask=mask) + + fit = al.FitImaging(dataset=masked_dataset, tracer=tracer, + settings=al.Settings(use_border_relocator=True) + ) + + assert fit.figure_of_merit == pytest.approx(565.6348654, 1.0e-2) + + +def test__simulate_imaging_data_and_fit__linear_light_profiles_agree_with_standard_light_profiles(): + + grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=1) + + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy = al.Galaxy( + redshift=1.0, + bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), + disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) + + dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) + dataset.noise_map = al.Array2D.ones( + shape_native=dataset.data.shape_native, pixel_scales=0.2 + ) + + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 + ) + + masked_dataset = dataset.apply_mask(mask=mask) + masked_dataset = masked_dataset.apply_over_sampling( + over_sample_size_lp=1 + ) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) + + lens_galaxy_linear = al.Galaxy( + redshift=0.5, + light=al.lp_linear.Sersic(centre=(0.1, 0.1)), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy_linear = al.Galaxy( + redshift=1.0, + bulge=al.lp_linear.Sersic(sersic_index=1.0), + disk=al.lp_linear.Sersic(sersic_index=4.0), + ) + + tracer_linear = al.Tracer( + galaxies=[lens_galaxy_linear, source_galaxy_linear] + ) + + fit_linear = al.FitImaging( + dataset=masked_dataset, + tracer=tracer_linear, + ) + + assert fit_linear.inversion.reconstruction == pytest.approx( + np.array([0.1, 0.1, 0.2]), 1.0e-4 + ) + assert fit_linear.linear_light_profile_intensity_dict[ + lens_galaxy_linear.light + ] == pytest.approx(0.1, 1.0e-2) + assert fit_linear.linear_light_profile_intensity_dict[ + source_galaxy_linear.bulge + ] == pytest.approx(0.1, 1.0e-2) + assert fit_linear.linear_light_profile_intensity_dict[ + source_galaxy_linear.disk + ] == pytest.approx(0.2, 1.0e-2) + assert fit.log_likelihood == fit_linear.figure_of_merit + assert fit_linear.figure_of_merit == pytest.approx(-45.02798, 1.0e-4) + + lens_galaxy_image = lens_galaxy.blurred_image_2d_from( + grid=masked_dataset.grids.lp, + psf=masked_dataset.psf, + blurring_grid=masked_dataset.grids.blurring, + ) + + assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( + lens_galaxy_image.array, 1.0e-4 + ) + assert fit_linear.model_images_of_planes_list[0] == pytest.approx( + lens_galaxy_image.array, 1.0e-4 + ) + + traced_grid_2d_list = tracer.traced_grid_2d_list_from(grid=masked_dataset.grids.lp) + traced_blurring_grid_2d_list = tracer.traced_grid_2d_list_from( + grid=masked_dataset.grids.blurring + ) + + source_galaxy_image = source_galaxy.blurred_image_2d_from( + grid=traced_grid_2d_list[1], + psf=masked_dataset.psf, + blurring_grid=traced_blurring_grid_2d_list[1], + ) + + assert fit_linear.galaxy_model_image_dict[source_galaxy_linear] == pytest.approx( + source_galaxy_image.array, 1.0e-4 + ) + + assert fit_linear.model_images_of_planes_list[1] == pytest.approx( + source_galaxy_image.array, 1.0e-4 + ) + + +def test__simulate_imaging_data_and_fit__linear_light_profiles_and_pixelization(): + + grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=1) + + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy = al.Galaxy( + redshift=1.0, + bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), + disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) + + dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) + dataset.noise_map = al.Array2D.ones( + shape_native=dataset.data.shape_native, pixel_scales=0.2 + ) + + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 + ) + + masked_dataset = dataset.apply_mask(mask=mask) + masked_dataset = masked_dataset.apply_over_sampling( + over_sample_size_lp=1 + ) + + lens_galaxy_linear = al.Galaxy( + redshift=0.5, + light=al.lp_linear.Sersic(centre=(0.1, 0.1)), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=0.01), + ) + + source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer_linear = al.Tracer( + galaxies=[lens_galaxy_linear, source_galaxy_pix] + ) + + fit_linear = al.FitImaging( + dataset=masked_dataset, + tracer=tracer_linear, + settings=al.Settings(use_border_relocator=True) + ) + + assert fit_linear.inversion.reconstruction[0:2] == pytest.approx( + np.array( + [ + 99.993449641, 0.114213814, + ] + ), + 1.0e-4, + ) + assert fit_linear.figure_of_merit == pytest.approx(-87.6933733814, 1.0e-4) + + lens_galaxy_image = lens_galaxy.blurred_image_2d_from( + grid=masked_dataset.grids.lp, + psf=masked_dataset.psf, + blurring_grid=masked_dataset.grids.blurring, + ) + + assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( + lens_galaxy_image.array, 1.0e-2 + ) + assert fit_linear.model_images_of_planes_list[0] == pytest.approx( + lens_galaxy_image.array, 1.0e-2 + ) + + assert fit_linear.galaxy_model_image_dict[source_galaxy_pix][0] == pytest.approx( + 0.0524952137, 1.0e-4 + ) + + assert fit_linear.model_images_of_planes_list[1][0] == pytest.approx( + 0.052495213, 1.0e-4 + ) + + fit_linear = al.FitImaging( + dataset=masked_dataset, + tracer=tracer_linear, + settings=al.Settings( + use_positive_only_solver=True, + ), + ) + + assert fit_linear.inversion.reconstruction == pytest.approx( + np.array( + [ + 100.01548, + 0.0, + 0.0, + 0.0, + 0.0, + 1.13328604, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ), + abs=1.0e-1, + ) + assert fit_linear.figure_of_merit == pytest.approx(-85.7890126577, 1.0e-4) + + +def test__simulate_imaging_data_and_fit__linear_light_profiles_and_pixelization__sub_2(): + + grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=2) + + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy = al.Galaxy( + redshift=1.0, + bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), + disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) + + dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) + dataset.noise_map = al.Array2D.ones( + shape_native=dataset.data.shape_native, pixel_scales=0.2 + ) + + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 + ) + + dataset = al.Imaging( + data=dataset.data, + psf=dataset.psf, + noise_map=dataset.noise_map, + over_sample_size_lp=2, + over_sample_size_pixelization=2 + ) + + masked_dataset = dataset.apply_mask(mask=mask) + + lens_galaxy_linear = al.Galaxy( + redshift=0.5, + light=al.lp_linear.Sersic(centre=(0.1, 0.1)), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=0.01), + ) + + source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer_linear = al.Tracer( + galaxies=[lens_galaxy_linear, source_galaxy_pix] + ) + + fit_linear = al.FitImaging( + dataset=masked_dataset, + tracer=tracer_linear, + settings=al.Settings(use_border_relocator=True) + ) + + assert fit_linear.inversion.reconstruction[0:2] == pytest.approx( + np.array( + [ + 99.974078996, 0.251635768, + ] + ), + 1.0e-4, + ) + + assert fit_linear.figure_of_merit == pytest.approx(-87.9411586204183, 1.0e-4) + + lens_galaxy_image = lens_galaxy.blurred_image_2d_from( + grid=masked_dataset.grids.lp, + psf=masked_dataset.psf, + blurring_grid=masked_dataset.grids.blurring, + ) + + assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( + lens_galaxy_image.array, 1.0e-2 + ) + assert fit_linear.model_images_of_planes_list[0] == pytest.approx( + lens_galaxy_image.array, 1.0e-2 + ) + + assert fit_linear.galaxy_model_image_dict[source_galaxy_pix][0] == pytest.approx( + 0.134961296372, 1.0e-4 + ) + + assert fit_linear.model_images_of_planes_list[1][0] == pytest.approx( + 0.134961296, 1.0e-4 + ) + + assert fit_linear.subtracted_images_of_planes_list[1][0] == pytest.approx( + 0.34355239059, 1.0e-4 + ) + + fit_linear = al.FitImaging( + dataset=masked_dataset, + tracer=tracer_linear, + settings=al.Settings( + use_positive_only_solver=True, + ), + ) + + assert fit_linear.inversion.reconstruction == pytest.approx( + np.array( + [ + 100.01548, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ), + abs=1.0e-1, + ) + assert fit_linear.figure_of_merit == pytest.approx(-86.95872610, abs=1.0e-4) + + +def test__simulate_imaging_data_and_fit__linear_light_profiles_and_pixelization__delaunay_split(): + + grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, over_sample_size=2) + + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy = al.Galaxy( + redshift=1.0, + bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), + disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) + + dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) + dataset.noise_map = al.Array2D.ones( + shape_native=dataset.data.shape_native, pixel_scales=0.2 + ) + + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.81 + ) + + dataset = al.Imaging( + data=dataset.data, + psf=dataset.psf, + noise_map=dataset.noise_map, + over_sample_size_lp=2, + over_sample_size_pixelization=2 + ) + + masked_dataset = dataset.apply_mask(mask=mask) + + lens_galaxy_linear = al.Galaxy( + redshift=0.5, + light=al.lp_linear.Sersic(centre=(0.1, 0.1)), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + + pixelization = al.Pixelization( + mesh=al.mesh.Delaunay(pixels=25, zeroed_pixels=5), + regularization=al.reg.AdaptSplit(inner_coefficient=0.01, outer_coefficient=0.1, signal_scale=0.1), + ) + + source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) + + image_mesh = al.image_mesh.Overlay(shape=(7, 7)) + + image_plane_mesh_grid = image_mesh.image_plane_mesh_grid_from( + mask=masked_dataset.mask, + ) + + adapt_images = al.AdaptImages( + galaxy_image_dict={source_galaxy_pix: masked_dataset.data}, + galaxy_image_plane_mesh_grid_dict={source_galaxy_pix: image_plane_mesh_grid}, + ) + + tracer_linear = al.Tracer( + galaxies=[lens_galaxy_linear, source_galaxy_pix] + ) + + fit_linear = al.FitImaging( + dataset=masked_dataset, + tracer=tracer_linear, + adapt_images=adapt_images, + settings=al.Settings(use_positive_only_solver=False), + ) + + assert fit_linear.inversion.reconstruction[0:3] == pytest.approx( + np.array( + [ + 1.00179579e+02, 5.35321466e-01, 8.55754143e-01 + ] + ), + 1.0e-4, + ) + + assert fit_linear.figure_of_merit == pytest.approx(-190.564548990939, 1.0e-4) + + lens_galaxy_image = lens_galaxy.blurred_image_2d_from( + grid=masked_dataset.grids.lp, + blurring_grid=masked_dataset.grids.blurring, + psf=masked_dataset.psf + ) + + assert fit_linear.galaxy_model_image_dict[lens_galaxy_linear] == pytest.approx( + lens_galaxy_image, 1.0e-2 + ) + assert fit_linear.model_images_of_planes_list[0] == pytest.approx( + lens_galaxy_image, 1.0e-2 + ) + + assert fit_linear.galaxy_model_image_dict[source_galaxy_pix][0] == pytest.approx( + 0.1667703826, 1.0e-4 + ) + + assert fit_linear.model_images_of_planes_list[1][0] == pytest.approx( + 0.166757208736973, 1.0e-4 + ) + + assert fit_linear.subtracted_images_of_planes_list[1][0] == pytest.approx( + 0.180018267146, 1.0e-4 + ) + + + fit_linear = al.FitImaging( + dataset=masked_dataset, + tracer=tracer_linear, + adapt_images=adapt_images, + settings=al.Settings( + use_positive_only_solver=True, + use_edge_zeroed_pixels=True + ), + ) + + assert fit_linear.inversion.reconstruction[0:2] == pytest.approx( + np.array( + [ + 99.9785287998059, + 0.8958653625423 + ] + ), + 1.0e-4, + ) + assert fit_linear.figure_of_merit == pytest.approx(-190.6935526756, 1.0e-4) + + +def test__fit_figure_of_merit__mge_mass_model(masked_imaging_7x7, masked_imaging_covariance_7x7): + + grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.2, + over_sample_size=8) + + psf = al.Convolver.from_gaussian( + shape_native=(3, 3), pixel_scales=0.2, sigma=0.75, normalize=True + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light_0=al.lp.Gaussian(intensity=1.0), + light_1=al.lp.Gaussian(intensity=2.0), + mass_0=al.mp.Gaussian(intensity=1.0, mass_to_light_ratio=3.0), + mass_1=al.mp.Gaussian(intensity=2.0, mass_to_light_ratio=4.0), + ) + source_galaxy = al.Galaxy( + redshift=1.0, light=al.lp.Exponential(centre=(0.1, 0.1), intensity=0.5) + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + dataset = al.SimulatorImaging(exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False) + + dataset = dataset.via_tracer_from(tracer=tracer, grid=grid) + dataset.noise_map = al.Array2D.ones( + shape_native=dataset.data.shape_native, pixel_scales=0.2 + ) + + file_path = Path(__file__).resolve().parent / "data_temp" / "simulate_and_fit" + + try: + shutil.rmtree(file_path) + except FileNotFoundError: + pass + + if not file_path.exists(): + os.makedirs(file_path) + + from autoarray.dataset.plot.imaging_plots import fits_imaging + + fits_imaging( + dataset=dataset, + data_path=file_path / "data.fits", + noise_map_path=file_path / "noise_map.fits", + psf_path=file_path / "psf.fits", + overwrite=True, + ) + + dataset = al.Imaging.from_fits( + data_path=file_path / "data.fits", + noise_map_path=file_path / "noise_map.fits", + psf_path=file_path / "psf.fits", + pixel_scales=0.2, + over_sample_size_lp=8 + ) + + mask = al.Mask2D.circular( + shape_native=dataset.data.shape_native, pixel_scales=0.2, radius=0.805 + ) + + masked_dataset = dataset.apply_mask(mask=mask) + + basis = al.lp_basis.Basis( + profile_list=[ + al.lmp.Gaussian(intensity=1.0, mass_to_light_ratio=3.0), + al.lmp.Gaussian(intensity=2.0, mass_to_light_ratio=4.0), + ] + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + bulge=basis, + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) + + # The value is actually not zero before the blurring grid assumes a sub_size=1 + # and does not use the iterative grid, which has a small impact on the chi-squared + + assert fit.chi_squared == pytest.approx(5.706423629698664e-05, 1e-4) + + masked_dataset = masked_dataset.apply_over_sampling( + over_sample_size_lp=8 + ) + + basis = al.lp_basis.Basis( + profile_list=[ + al.lmp_linear.Gaussian(intensity=1.0, mass_to_light_ratio=3.0), + al.lmp_linear.Gaussian(intensity=2.0, mass_to_light_ratio=4.0), + ] + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + bulge=basis, + ) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + fit = al.FitImaging(dataset=masked_dataset, tracer=tracer) + + # The value is actually not zero before the blurring grid assumes a sub_size=1 + # and does not use the iterative grid, which has a small impact on the chi-squared + + assert fit.chi_squared == pytest.approx(3.295535243634485e-05, 1e-4) + + file_path = Path(__file__).resolve().parent / "data_temp" + + if file_path.exists(): shutil.rmtree(file_path) def test__perfect_fit__chi_squared_0__oversampled_psf(): diff --git a/test_autolens/interferometer/model/test_result_interferometer.py b/test_autolens/interferometer/model/test_result_interferometer.py index 367e5f51d..8a5fe827d 100644 --- a/test_autolens/interferometer/model/test_result_interferometer.py +++ b/test_autolens/interferometer/model/test_result_interferometer.py @@ -1,8 +1,8 @@ -from pathlib import Path - - -directory = Path(__file__).resolve().parent - - -class TestResultInterferometer: - pass +from pathlib import Path + + +directory = Path(__file__).resolve().parent + + +class TestResultInterferometer: + pass diff --git a/test_autolens/interferometer/test_fit_interferometer.py b/test_autolens/interferometer/test_fit_interferometer.py index f13e1ad03..46cef27ce 100644 --- a/test_autolens/interferometer/test_fit_interferometer.py +++ b/test_autolens/interferometer/test_fit_interferometer.py @@ -1,388 +1,388 @@ -import numpy as np -import pytest - -import autolens as al - - -def test__model_visibilities(interferometer_7): - g0 = al.Galaxy(redshift=0.5, bulge=al.m.MockLightProfile(image_2d=np.ones(9))) - tracer = al.Tracer(galaxies=[g0]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.model_data.slim[0].real == pytest.approx(1.48496, abs=1.0e-4) - assert fit.model_data.slim[0].imag == pytest.approx(0.0, abs=1.0e-4) - assert fit.log_likelihood == pytest.approx(-34.1685958, abs=1.0e-4) - - -def test__fit_figure_of_merit(interferometer_7): - # TODO : Use pytest.parameterize - - g0 = al.Galaxy( - redshift=0.5, - bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), - disk=al.lp.Sersic(centre=(0.05, 0.05), intensity=2.0), - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - - g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(intensity=1.0)) - - tracer = al.Tracer(galaxies=[g0, g1]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.perform_inversion is False - assert fit.figure_of_merit == pytest.approx(-12758.714175708, 1.0e-4) - - basis = al.lp_basis.Basis( - profile_list=[ - al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), - al.lp.Sersic(centre=(0.05, 0.05), intensity=2.0), - ] - ) - - g0 = al.Galaxy( - redshift=0.5, - bulge=basis, - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - - g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0)) - - tracer = al.Tracer(galaxies=[g0, g1]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.perform_inversion is False - assert fit.figure_of_merit == pytest.approx(-12779.937568696, 1.0e-4) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=0.01), - ) - - g0 = al.Galaxy(redshift=0.5, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g0]) - - fit = al.FitInterferometer( - dataset=interferometer_7, - tracer=tracer, - ) - - assert fit.perform_inversion is True - assert fit.figure_of_merit == pytest.approx(-71.7704487241, 1.0e-4) - - galaxy_light = al.Galaxy( - redshift=0.5, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0) - ) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=1.0), - ) - - galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[galaxy_light, galaxy_pix]) - - fit = al.FitInterferometer( - dataset=interferometer_7, - tracer=tracer, - ) - - assert fit.perform_inversion is True - assert fit.figure_of_merit == pytest.approx(-196.15073725528, 1.0e-4) - - g0_linear = al.Galaxy( - redshift=0.5, - bulge=al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=1.0), - disk=al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=4.0), - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - - tracer = al.Tracer(galaxies=[g0_linear, g1]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.perform_inversion is True - assert fit.figure_of_merit == pytest.approx(-197.670468767, 1.0e-4) - - basis = al.lp_basis.Basis( - profile_list=[ - al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=1.0), - al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=4.0), - ] - ) - - g0_linear = al.Galaxy( - redshift=0.5, - bulge=basis, - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - - tracer = al.Tracer(galaxies=[g0_linear, g1]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.perform_inversion is True - assert fit.figure_of_merit == pytest.approx(-197.6704687, 1.0e-4) - - tracer = al.Tracer(galaxies=[g0_linear, galaxy_pix]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.perform_inversion is True - assert fit.figure_of_merit == pytest.approx(-35.07914066930113, 1.0e-4) - - -def test___galaxy_image_dict(interferometer_7, interferometer_7_grid): - # Normal Light Profiles Only - - g0 = al.Galaxy( - redshift=0.5, - bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0)) - g2 = al.Galaxy(redshift=1.0) - - tracer = al.Tracer(galaxies=[g0, g1, g2]) - - fit = al.FitInterferometer( - dataset=interferometer_7_grid, - tracer=tracer, - ) - - traced_grid_2d_list_from = tracer.traced_grid_2d_list_from( - grid=interferometer_7.grids.lp - ) - - g0_image = g0.image_2d_from(grid=traced_grid_2d_list_from[0]) - g1_image = g1.image_2d_from(grid=traced_grid_2d_list_from[1]) - - assert fit.galaxy_image_dict[g0] == pytest.approx(g0_image.array, 1.0e-4) - assert fit.galaxy_image_dict[g1] == pytest.approx(g1_image.array, 1.0e-4) - - # Linear Light Profiles Only - - g0_linear = al.Galaxy( - redshift=0.5, - bulge=al.lp_linear.Sersic(centre=(0.05, 0.05)), - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - g1_linear = al.Galaxy(redshift=1.0, bulge=al.lp_linear.Sersic()) - - tracer = al.Tracer(galaxies=[g0_linear, g1_linear, g2]) - - fit = al.FitInterferometer( - dataset=interferometer_7_grid, - tracer=tracer, - ) - - assert fit.galaxy_image_dict[g0_linear][4] == pytest.approx(1.00018622848, 1.0e-2) - assert fit.galaxy_image_dict[g1_linear][3] == pytest.approx(-0.017435532289, 1.0e-2) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=1.0), - ) - - g0_no_light = al.Galaxy( - redshift=0.5, - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[g0_no_light, galaxy_pix_0]) - - fit = al.FitInterferometer( - dataset=interferometer_7, - tracer=tracer, - ) - - assert (fit.galaxy_image_dict[g0_no_light].native == np.zeros((7, 7))).all() - - assert fit.galaxy_image_dict[galaxy_pix_0][0] == pytest.approx( - -0.14416215690290285, 1.0e-4 - ) - - # Normal light + Linear Light PRofiles + Pixelization + Regularization - - galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization) - tracer = al.Tracer(galaxies=[g0, g0_linear, g2, galaxy_pix_0, galaxy_pix_1]) - - fit = al.FitInterferometer( - dataset=interferometer_7_grid, - tracer=tracer, - ) - - assert fit.galaxy_image_dict[g0] == pytest.approx(g0_image.array, 1.0e-4) - - assert fit.galaxy_image_dict[g0_linear][4] == pytest.approx( - -22.896762784783178, 1.0e-4 - ) - - assert fit.galaxy_image_dict[galaxy_pix_0][4] == pytest.approx( - -0.027972920686385083, 1.0e-3 - ) - assert fit.galaxy_image_dict[galaxy_pix_1][4] == pytest.approx( - -0.02797291718230969, 1.0e-3 - ) - assert (fit.galaxy_image_dict[g2] == np.zeros(9)).all() - - -def test__galaxy_model_visibilities_dict(interferometer_7, interferometer_7_grid): - # Normal Light Profiles Only - - g0 = al.Galaxy( - redshift=0.5, - bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0)) - g2 = al.Galaxy(redshift=1.0) - - tracer = al.Tracer(galaxies=[g0, g1, g2]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - traced_grid_2d_list_from = tracer.traced_grid_2d_list_from( - grid=interferometer_7.grids.lp - ) - - g0_profile_visibilities = g0.visibilities_from( - grid=traced_grid_2d_list_from[0], transformer=interferometer_7_grid.transformer - ) - - g1_profile_visibilities = g1.visibilities_from( - grid=traced_grid_2d_list_from[1], transformer=interferometer_7_grid.transformer - ) - - assert fit.galaxy_model_visibilities_dict[g0].slim.array == pytest.approx( - g0_profile_visibilities.array, 1.0e-4 - ) - assert fit.galaxy_model_visibilities_dict[g1].slim.array == pytest.approx( - g1_profile_visibilities.array, 1.0e-4 - ) - assert ( - fit.galaxy_model_visibilities_dict[g2].slim.array - == (0.0 + 0.0j) * np.zeros((7,)) - ).all() - - assert fit.model_data.slim.array == pytest.approx( - fit.galaxy_model_visibilities_dict[g0].slim.array - + fit.galaxy_model_visibilities_dict[g1].slim.array, - 1.0e-4, - ) - - # Linear Light Profiles Only - - g0_linear = al.Galaxy( - redshift=0.5, - bulge=al.lp_linear.Sersic(centre=(0.05, 0.05)), - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - g1_linear = al.Galaxy(redshift=1.0, bulge=al.lp_linear.Sersic(centre=(0.05, 0.05))) - - tracer = al.Tracer(galaxies=[g0_linear, g1_linear, g2]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.galaxy_model_visibilities_dict[g0_linear][0] == pytest.approx( - 1.0138228768598911 + 0.006599377953512708j, 1.0e-2 - ) - assert fit.galaxy_model_visibilities_dict[g1_linear][0] == pytest.approx( - -0.012892097547972572 - 0.0019719184145301906j, 1.0e-2 - ) - assert (fit.galaxy_model_visibilities_dict[g2] == np.zeros((7,))).all() - - assert fit.model_data.array == pytest.approx( - fit.galaxy_model_visibilities_dict[g0_linear].array - + fit.galaxy_model_visibilities_dict[g1_linear].array, - 1.0e-4, - ) - - # Pixelization + Regularizaiton only - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=1.0), - ) - - g0_no_light = al.Galaxy( - redshift=0.5, - mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), - ) - galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[g0_no_light, galaxy_pix_0]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert (fit.galaxy_model_visibilities_dict[g0_no_light] == np.zeros((7,))).all() - assert fit.galaxy_model_visibilities_dict[galaxy_pix_0][0] == pytest.approx( - 0.9372276032882099 + 0.32701101190914555j, 1.0e-4 - ) - - assert fit.model_data.array == pytest.approx( - fit.galaxy_model_visibilities_dict[galaxy_pix_0].array, 1.0e-4 - ) - - # Normal light + Linear Light PRofiles + Pixelization + Regularizaiton - - galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[g0, g0_linear, g2, galaxy_pix_0, galaxy_pix_1]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.galaxy_model_visibilities_dict[g0].array == pytest.approx( - g0_profile_visibilities.array, 1.0e-4 - ) - - assert fit.galaxy_model_visibilities_dict[g0_linear][0] == pytest.approx( - -23.101974527607315 - 0.15037997746935183j, 1.0e-4 - ) - - assert fit.galaxy_model_visibilities_dict[galaxy_pix_0][0] == pytest.approx( - -0.06797397090116747 + 0.0892532970401005j, 1.0e-4 - ) - assert fit.galaxy_model_visibilities_dict[galaxy_pix_1][0] == pytest.approx( - -0.06797396569775271 + 0.08925329704010045j, 1.0e-4 - ) - assert (fit.galaxy_model_visibilities_dict[g2] == np.zeros((7,))).all() - - -def test__model_visibilities_of_planes_list(interferometer_7): - g0 = al.Galaxy( - redshift=0.5, - bulge=al.lp.Sersic(intensity=1.0), - mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), - ) - - g1_linear = al.Galaxy(redshift=0.75, bulge=al.lp_linear.Sersic()) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=1.0), - ) - - galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) - galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[g0, g1_linear, galaxy_pix_0, galaxy_pix_1]) - - fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) - - assert fit.model_visibilities_of_planes_list[0].array == pytest.approx( - fit.galaxy_model_visibilities_dict[g0].array, 1.0e-4 - ) - assert fit.model_visibilities_of_planes_list[1].array == pytest.approx( - fit.galaxy_model_visibilities_dict[g1_linear].array, 1.0e-4 - ) - assert fit.model_visibilities_of_planes_list[2].array == pytest.approx( - fit.galaxy_model_visibilities_dict[galaxy_pix_0].array - + fit.galaxy_model_visibilities_dict[galaxy_pix_1].array, - 1.0e-4, - ) +import numpy as np +import pytest + +import autolens as al + + +def test__model_visibilities(interferometer_7): + g0 = al.Galaxy(redshift=0.5, bulge=al.m.MockLightProfile(image_2d=np.ones(9))) + tracer = al.Tracer(galaxies=[g0]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.model_data.slim[0].real == pytest.approx(1.48496, abs=1.0e-4) + assert fit.model_data.slim[0].imag == pytest.approx(0.0, abs=1.0e-4) + assert fit.log_likelihood == pytest.approx(-34.1685958, abs=1.0e-4) + + +def test__fit_figure_of_merit(interferometer_7): + # TODO : Use pytest.parameterize + + g0 = al.Galaxy( + redshift=0.5, + bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), + disk=al.lp.Sersic(centre=(0.05, 0.05), intensity=2.0), + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + + g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(intensity=1.0)) + + tracer = al.Tracer(galaxies=[g0, g1]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.perform_inversion is False + assert fit.figure_of_merit == pytest.approx(-12758.714175708, 1.0e-4) + + basis = al.lp_basis.Basis( + profile_list=[ + al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), + al.lp.Sersic(centre=(0.05, 0.05), intensity=2.0), + ] + ) + + g0 = al.Galaxy( + redshift=0.5, + bulge=basis, + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + + g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0)) + + tracer = al.Tracer(galaxies=[g0, g1]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.perform_inversion is False + assert fit.figure_of_merit == pytest.approx(-12779.937568696, 1.0e-4) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=0.01), + ) + + g0 = al.Galaxy(redshift=0.5, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g0]) + + fit = al.FitInterferometer( + dataset=interferometer_7, + tracer=tracer, + ) + + assert fit.perform_inversion is True + assert fit.figure_of_merit == pytest.approx(-71.7704487241, 1.0e-4) + + galaxy_light = al.Galaxy( + redshift=0.5, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0) + ) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=1.0), + ) + + galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[galaxy_light, galaxy_pix]) + + fit = al.FitInterferometer( + dataset=interferometer_7, + tracer=tracer, + ) + + assert fit.perform_inversion is True + assert fit.figure_of_merit == pytest.approx(-196.15073725528, 1.0e-4) + + g0_linear = al.Galaxy( + redshift=0.5, + bulge=al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=1.0), + disk=al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=4.0), + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + + tracer = al.Tracer(galaxies=[g0_linear, g1]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.perform_inversion is True + assert fit.figure_of_merit == pytest.approx(-197.670468767, 1.0e-4) + + basis = al.lp_basis.Basis( + profile_list=[ + al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=1.0), + al.lp_linear.Sersic(centre=(0.05, 0.05), sersic_index=4.0), + ] + ) + + g0_linear = al.Galaxy( + redshift=0.5, + bulge=basis, + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + + tracer = al.Tracer(galaxies=[g0_linear, g1]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.perform_inversion is True + assert fit.figure_of_merit == pytest.approx(-197.6704687, 1.0e-4) + + tracer = al.Tracer(galaxies=[g0_linear, galaxy_pix]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.perform_inversion is True + assert fit.figure_of_merit == pytest.approx(-35.07914066930113, 1.0e-4) + + +def test___galaxy_image_dict(interferometer_7, interferometer_7_grid): + # Normal Light Profiles Only + + g0 = al.Galaxy( + redshift=0.5, + bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0)) + g2 = al.Galaxy(redshift=1.0) + + tracer = al.Tracer(galaxies=[g0, g1, g2]) + + fit = al.FitInterferometer( + dataset=interferometer_7_grid, + tracer=tracer, + ) + + traced_grid_2d_list_from = tracer.traced_grid_2d_list_from( + grid=interferometer_7.grids.lp + ) + + g0_image = g0.image_2d_from(grid=traced_grid_2d_list_from[0]) + g1_image = g1.image_2d_from(grid=traced_grid_2d_list_from[1]) + + assert fit.galaxy_image_dict[g0] == pytest.approx(g0_image.array, 1.0e-4) + assert fit.galaxy_image_dict[g1] == pytest.approx(g1_image.array, 1.0e-4) + + # Linear Light Profiles Only + + g0_linear = al.Galaxy( + redshift=0.5, + bulge=al.lp_linear.Sersic(centre=(0.05, 0.05)), + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + g1_linear = al.Galaxy(redshift=1.0, bulge=al.lp_linear.Sersic()) + + tracer = al.Tracer(galaxies=[g0_linear, g1_linear, g2]) + + fit = al.FitInterferometer( + dataset=interferometer_7_grid, + tracer=tracer, + ) + + assert fit.galaxy_image_dict[g0_linear][4] == pytest.approx(1.00018622848, 1.0e-2) + assert fit.galaxy_image_dict[g1_linear][3] == pytest.approx(-0.017435532289, 1.0e-2) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=1.0), + ) + + g0_no_light = al.Galaxy( + redshift=0.5, + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[g0_no_light, galaxy_pix_0]) + + fit = al.FitInterferometer( + dataset=interferometer_7, + tracer=tracer, + ) + + assert (fit.galaxy_image_dict[g0_no_light].native == np.zeros((7, 7))).all() + + assert fit.galaxy_image_dict[galaxy_pix_0][0] == pytest.approx( + -0.14416215690290285, 1.0e-4 + ) + + # Normal light + Linear Light PRofiles + Pixelization + Regularization + + galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization) + tracer = al.Tracer(galaxies=[g0, g0_linear, g2, galaxy_pix_0, galaxy_pix_1]) + + fit = al.FitInterferometer( + dataset=interferometer_7_grid, + tracer=tracer, + ) + + assert fit.galaxy_image_dict[g0] == pytest.approx(g0_image.array, 1.0e-4) + + assert fit.galaxy_image_dict[g0_linear][4] == pytest.approx( + -22.896762784783178, 1.0e-4 + ) + + assert fit.galaxy_image_dict[galaxy_pix_0][4] == pytest.approx( + -0.027972920686385083, 1.0e-3 + ) + assert fit.galaxy_image_dict[galaxy_pix_1][4] == pytest.approx( + -0.02797291718230969, 1.0e-3 + ) + assert (fit.galaxy_image_dict[g2] == np.zeros(9)).all() + + +def test__galaxy_model_visibilities_dict(interferometer_7, interferometer_7_grid): + # Normal Light Profiles Only + + g0 = al.Galaxy( + redshift=0.5, + bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0), + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + g1 = al.Galaxy(redshift=1.0, bulge=al.lp.Sersic(centre=(0.05, 0.05), intensity=1.0)) + g2 = al.Galaxy(redshift=1.0) + + tracer = al.Tracer(galaxies=[g0, g1, g2]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + traced_grid_2d_list_from = tracer.traced_grid_2d_list_from( + grid=interferometer_7.grids.lp + ) + + g0_profile_visibilities = g0.visibilities_from( + grid=traced_grid_2d_list_from[0], transformer=interferometer_7_grid.transformer + ) + + g1_profile_visibilities = g1.visibilities_from( + grid=traced_grid_2d_list_from[1], transformer=interferometer_7_grid.transformer + ) + + assert fit.galaxy_model_visibilities_dict[g0].slim.array == pytest.approx( + g0_profile_visibilities.array, 1.0e-4 + ) + assert fit.galaxy_model_visibilities_dict[g1].slim.array == pytest.approx( + g1_profile_visibilities.array, 1.0e-4 + ) + assert ( + fit.galaxy_model_visibilities_dict[g2].slim.array + == (0.0 + 0.0j) * np.zeros((7,)) + ).all() + + assert fit.model_data.slim.array == pytest.approx( + fit.galaxy_model_visibilities_dict[g0].slim.array + + fit.galaxy_model_visibilities_dict[g1].slim.array, + 1.0e-4, + ) + + # Linear Light Profiles Only + + g0_linear = al.Galaxy( + redshift=0.5, + bulge=al.lp_linear.Sersic(centre=(0.05, 0.05)), + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + g1_linear = al.Galaxy(redshift=1.0, bulge=al.lp_linear.Sersic(centre=(0.05, 0.05))) + + tracer = al.Tracer(galaxies=[g0_linear, g1_linear, g2]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.galaxy_model_visibilities_dict[g0_linear][0] == pytest.approx( + 1.0138228768598911 + 0.006599377953512708j, 1.0e-2 + ) + assert fit.galaxy_model_visibilities_dict[g1_linear][0] == pytest.approx( + -0.012892097547972572 - 0.0019719184145301906j, 1.0e-2 + ) + assert (fit.galaxy_model_visibilities_dict[g2] == np.zeros((7,))).all() + + assert fit.model_data.array == pytest.approx( + fit.galaxy_model_visibilities_dict[g0_linear].array + + fit.galaxy_model_visibilities_dict[g1_linear].array, + 1.0e-4, + ) + + # Pixelization + Regularizaiton only + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=1.0), + ) + + g0_no_light = al.Galaxy( + redshift=0.5, + mass_profile=al.mp.IsothermalSph(centre=(0.05, 0.05), einstein_radius=1.0), + ) + galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[g0_no_light, galaxy_pix_0]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert (fit.galaxy_model_visibilities_dict[g0_no_light] == np.zeros((7,))).all() + assert fit.galaxy_model_visibilities_dict[galaxy_pix_0][0] == pytest.approx( + 0.9372276032882099 + 0.32701101190914555j, 1.0e-4 + ) + + assert fit.model_data.array == pytest.approx( + fit.galaxy_model_visibilities_dict[galaxy_pix_0].array, 1.0e-4 + ) + + # Normal light + Linear Light PRofiles + Pixelization + Regularizaiton + + galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[g0, g0_linear, g2, galaxy_pix_0, galaxy_pix_1]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.galaxy_model_visibilities_dict[g0].array == pytest.approx( + g0_profile_visibilities.array, 1.0e-4 + ) + + assert fit.galaxy_model_visibilities_dict[g0_linear][0] == pytest.approx( + -23.101974527607315 - 0.15037997746935183j, 1.0e-4 + ) + + assert fit.galaxy_model_visibilities_dict[galaxy_pix_0][0] == pytest.approx( + -0.06797397090116747 + 0.0892532970401005j, 1.0e-4 + ) + assert fit.galaxy_model_visibilities_dict[galaxy_pix_1][0] == pytest.approx( + -0.06797396569775271 + 0.08925329704010045j, 1.0e-4 + ) + assert (fit.galaxy_model_visibilities_dict[g2] == np.zeros((7,))).all() + + +def test__model_visibilities_of_planes_list(interferometer_7): + g0 = al.Galaxy( + redshift=0.5, + bulge=al.lp.Sersic(intensity=1.0), + mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), + ) + + g1_linear = al.Galaxy(redshift=0.75, bulge=al.lp_linear.Sersic()) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=1.0), + ) + + galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) + galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[g0, g1_linear, galaxy_pix_0, galaxy_pix_1]) + + fit = al.FitInterferometer(dataset=interferometer_7, tracer=tracer) + + assert fit.model_visibilities_of_planes_list[0].array == pytest.approx( + fit.galaxy_model_visibilities_dict[g0].array, 1.0e-4 + ) + assert fit.model_visibilities_of_planes_list[1].array == pytest.approx( + fit.galaxy_model_visibilities_dict[g1_linear].array, 1.0e-4 + ) + assert fit.model_visibilities_of_planes_list[2].array == pytest.approx( + fit.galaxy_model_visibilities_dict[galaxy_pix_0].array + + fit.galaxy_model_visibilities_dict[galaxy_pix_1].array, + 1.0e-4, + ) diff --git a/test_autolens/interferometer/test_simulate_and_fit_interferometer.py b/test_autolens/interferometer/test_simulate_and_fit_interferometer.py index 172a3b5d6..f9f532856 100644 --- a/test_autolens/interferometer/test_simulate_and_fit_interferometer.py +++ b/test_autolens/interferometer/test_simulate_and_fit_interferometer.py @@ -1,316 +1,316 @@ -import os -from pathlib import Path -import shutil - -import autolens as al -import numpy as np -import pytest - - -def test__perfect_fit__chi_squared_0(): - grid = al.Grid2D.uniform( - shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1 - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), - ) - - source_galaxy = al.Galaxy( - redshift=1.0, light=al.lp.Exponential(centre=(0.1, 0.1), intensity=0.5) - ) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - simulator = al.SimulatorInterferometer( - uv_wavelengths=np.ones(shape=(7, 2)), - transformer_class=al.TransformerDFT, - exposure_time=300.0, - noise_if_add_noise_false=1.0, - noise_sigma=None, - ) - - dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) - - file_path = Path(__file__).resolve().parent / "data_temp" / "simulate_and_fit" - - try: - shutil.rmtree(file_path) - except FileNotFoundError: - pass - - if not file_path.exists(): - os.makedirs(file_path) - - from autoarray.dataset.plot.interferometer_plots import fits_interferometer - - fits_interferometer( - dataset=dataset, - data_path=file_path / "data.fits", - noise_map_path=file_path / "noise_map.fits", - uv_wavelengths_path=file_path / "uv_wavelengths.fits", - overwrite=True, - ) - - real_space_mask = al.Mask2D.all_false( - shape_native=(51, 51), - pixel_scales=0.1, - ) - - dataset = al.Interferometer.from_fits( - data_path=file_path / "data.fits", - noise_map_path=file_path / "noise_map.fits", - uv_wavelengths_path=file_path / "uv_wavelengths.fits", - real_space_mask=real_space_mask, - transformer_class=al.TransformerDFT, - ) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - fit = al.FitInterferometer( - dataset=dataset, - tracer=tracer, - ) - - assert fit.chi_squared == pytest.approx(0.0) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(7, 7)), - regularization=al.reg.Constant(coefficient=0.0001), - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), - ) - - source_galaxy = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - fit = al.FitInterferometer( - dataset=dataset, - tracer=tracer, - ) - assert abs(fit.chi_squared) < 1.0e-4 - - file_path = Path(__file__).resolve().parent / "data_temp" - - if file_path.exists(): - shutil.rmtree(file_path) - - -def test__simulate_interferometer_data_and_fit__known_likelihood(): - mask = al.Mask2D.circular(radius=3.0, shape_native=(31, 31), pixel_scales=0.2) - - grid = al.Grid2D.from_mask(mask=mask, over_sample_size=1) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(16, 16)), - regularization=al.reg.Constant(coefficient=1.0), - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), - ) - source_galaxy_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) - source_galaxy_1 = al.Galaxy(redshift=2.0, pixelization=pixelization) - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy_0, source_galaxy_1]) - - simulator = al.SimulatorInterferometer( - uv_wavelengths=np.ones(shape=(7, 2)), - transformer_class=al.TransformerDFT, - exposure_time=300.0, - noise_seed=1, - ) - - dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) - - fit = al.FitInterferometer( - dataset=dataset, - tracer=tracer, - ) - - assert fit.figure_of_merit == pytest.approx(-5.433894158056919, 1.0e-2) - - -def test__simulate_interferometer_data_and_fit__linear_light_profiles_agree_with_standard_light_profiles(): - grid = al.Grid2D.uniform( - shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1 - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), - ) - - source_galaxy = al.Galaxy( - redshift=1.0, - bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), - disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), - ) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - simulator = al.SimulatorInterferometer( - uv_wavelengths=np.array( - [ - [0.04, 200.0, 0.3, 400000.0, 60000000.0], - [0.00003, 500.0, 600000.0, 0.1, 75555555], - ] - ), - transformer_class=al.TransformerDFT, - exposure_time=300.0, - noise_if_add_noise_false=1.0, - noise_sigma=None, - ) - - dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) - - fit = al.FitInterferometer( - dataset=dataset, - tracer=tracer, - ) - - lens_galaxy_linear = al.Galaxy( - redshift=0.5, - light=al.lp_linear.Sersic(centre=(0.1, 0.1)), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), - ) - - source_galaxy_linear = al.Galaxy( - redshift=1.0, - bulge=al.lp_linear.Sersic(sersic_index=1.0), - disk=al.lp_linear.Sersic(sersic_index=4.0), - ) - - tracer_linear = al.Tracer(galaxies=[lens_galaxy_linear, source_galaxy_linear]) - - fit_linear = al.FitInterferometer( - dataset=dataset, - tracer=tracer_linear, - ) - - assert fit_linear.inversion.reconstruction == pytest.approx( - np.array([0.1, 0.1, 0.2]), 1.0e-4 - ) - assert fit_linear.linear_light_profile_intensity_dict[ - lens_galaxy_linear.light - ] == pytest.approx(0.1, 1.0e-2) - assert fit_linear.linear_light_profile_intensity_dict[ - source_galaxy_linear.bulge - ] == pytest.approx(0.1, 1.0e-2) - assert fit_linear.linear_light_profile_intensity_dict[ - source_galaxy_linear.disk - ] == pytest.approx(0.2, 1.0e-2) - assert fit.log_likelihood == pytest.approx(fit_linear.log_likelihood) - - lens_galaxy_image = lens_galaxy.image_2d_from(grid=dataset.grids.lp) - - assert fit_linear.galaxy_image_dict[lens_galaxy_linear] == pytest.approx( - lens_galaxy_image.array, 1.0e-4 - ) - - traced_grid_2d_list = tracer.traced_grid_2d_list_from(grid=dataset.grids.lp) - - source_galaxy_image = source_galaxy.image_2d_from(grid=traced_grid_2d_list[1]) - - assert fit_linear.galaxy_image_dict[source_galaxy_linear] == pytest.approx( - source_galaxy_image.array, 1.0e-4 - ) - - lens_galaxy_visibilities = lens_galaxy.visibilities_from( - grid=dataset.grids.lp, transformer=dataset.transformer - ) - - assert fit_linear.galaxy_model_visibilities_dict[ - lens_galaxy_linear - ].array == pytest.approx(lens_galaxy_visibilities.array, 1.0e-4) - - source_galaxy_visibilities = source_galaxy.visibilities_from( - grid=traced_grid_2d_list[1], transformer=dataset.transformer - ) - - assert fit_linear.galaxy_model_visibilities_dict[ - source_galaxy_linear - ].array == pytest.approx(source_galaxy_visibilities.array, 1.0e-4) - - -def test__simulate_interferometer_data_and_fit__linear_light_profiles_and_pixelization(): - grid = al.Grid2D.uniform( - shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1 - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), - ) - - source_galaxy = al.Galaxy( - redshift=1.0, - bulge=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1, sersic_index=1.0), - disk=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.2, sersic_index=4.0), - ) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - simulator = al.SimulatorInterferometer( - uv_wavelengths=np.array( - [ - [0.04, 200.0, 0.3, 400000.0, 60000000.0], - [0.00003, 500.0, 600000.0, 0.1, 75555555], - ] - ), - transformer_class=al.TransformerDFT, - exposure_time=300.0, - noise_if_add_noise_false=1.0, - noise_sigma=None, - ) - - dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) - - lens_galaxy_linear = al.Galaxy( - redshift=0.5, - light=al.lp_linear.Sersic(centre=(0.1, 0.1)), - mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), - ) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=0.01), - ) - - source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) - - tracer_linear = al.Tracer(galaxies=[lens_galaxy_linear, source_galaxy_pix]) - - fit_linear = al.FitInterferometer( - dataset=dataset, - tracer=tracer_linear, - ) - - assert fit_linear.inversion.reconstruction == pytest.approx( - np.array( - [ - 101.76664331, - 0.49639672, - 0.49531196, - 0.49854243, - 0.44661417, - 0.44782337, - 0.44844437, - 0.39942579, - 0.40320996, - 0.40104302, - ] - ), - 1.0e-2, - ) - assert fit_linear.figure_of_merit == pytest.approx(-29.223696823166, 1.0e-4) +import os +from pathlib import Path +import shutil + +import autolens as al +import numpy as np +import pytest + + +def test__perfect_fit__chi_squared_0(): + grid = al.Grid2D.uniform( + shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1 + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), + ) + + source_galaxy = al.Galaxy( + redshift=1.0, light=al.lp.Exponential(centre=(0.1, 0.1), intensity=0.5) + ) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + simulator = al.SimulatorInterferometer( + uv_wavelengths=np.ones(shape=(7, 2)), + transformer_class=al.TransformerDFT, + exposure_time=300.0, + noise_if_add_noise_false=1.0, + noise_sigma=None, + ) + + dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) + + file_path = Path(__file__).resolve().parent / "data_temp" / "simulate_and_fit" + + try: + shutil.rmtree(file_path) + except FileNotFoundError: + pass + + if not file_path.exists(): + os.makedirs(file_path) + + from autoarray.dataset.plot.interferometer_plots import fits_interferometer + + fits_interferometer( + dataset=dataset, + data_path=file_path / "data.fits", + noise_map_path=file_path / "noise_map.fits", + uv_wavelengths_path=file_path / "uv_wavelengths.fits", + overwrite=True, + ) + + real_space_mask = al.Mask2D.all_false( + shape_native=(51, 51), + pixel_scales=0.1, + ) + + dataset = al.Interferometer.from_fits( + data_path=file_path / "data.fits", + noise_map_path=file_path / "noise_map.fits", + uv_wavelengths_path=file_path / "uv_wavelengths.fits", + real_space_mask=real_space_mask, + transformer_class=al.TransformerDFT, + ) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + fit = al.FitInterferometer( + dataset=dataset, + tracer=tracer, + ) + + assert fit.chi_squared == pytest.approx(0.0) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(7, 7)), + regularization=al.reg.Constant(coefficient=0.0001), + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), + ) + + source_galaxy = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + fit = al.FitInterferometer( + dataset=dataset, + tracer=tracer, + ) + assert abs(fit.chi_squared) < 1.0e-4 + + file_path = Path(__file__).resolve().parent / "data_temp" + + if file_path.exists(): + shutil.rmtree(file_path) + + +def test__simulate_interferometer_data_and_fit__known_likelihood(): + mask = al.Mask2D.circular(radius=3.0, shape_native=(31, 31), pixel_scales=0.2) + + grid = al.Grid2D.from_mask(mask=mask, over_sample_size=1) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(16, 16)), + regularization=al.reg.Constant(coefficient=1.0), + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.8), + ) + source_galaxy_0 = al.Galaxy(redshift=1.0, pixelization=pixelization) + source_galaxy_1 = al.Galaxy(redshift=2.0, pixelization=pixelization) + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy_0, source_galaxy_1]) + + simulator = al.SimulatorInterferometer( + uv_wavelengths=np.ones(shape=(7, 2)), + transformer_class=al.TransformerDFT, + exposure_time=300.0, + noise_seed=1, + ) + + dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) + + fit = al.FitInterferometer( + dataset=dataset, + tracer=tracer, + ) + + assert fit.figure_of_merit == pytest.approx(-5.433894158056919, 1.0e-2) + + +def test__simulate_interferometer_data_and_fit__linear_light_profiles_agree_with_standard_light_profiles(): + grid = al.Grid2D.uniform( + shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1 + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), + ) + + source_galaxy = al.Galaxy( + redshift=1.0, + bulge=al.lp.Sersic(intensity=0.1, sersic_index=1.0), + disk=al.lp.Sersic(intensity=0.2, sersic_index=4.0), + ) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + simulator = al.SimulatorInterferometer( + uv_wavelengths=np.array( + [ + [0.04, 200.0, 0.3, 400000.0, 60000000.0], + [0.00003, 500.0, 600000.0, 0.1, 75555555], + ] + ), + transformer_class=al.TransformerDFT, + exposure_time=300.0, + noise_if_add_noise_false=1.0, + noise_sigma=None, + ) + + dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) + + fit = al.FitInterferometer( + dataset=dataset, + tracer=tracer, + ) + + lens_galaxy_linear = al.Galaxy( + redshift=0.5, + light=al.lp_linear.Sersic(centre=(0.1, 0.1)), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), + ) + + source_galaxy_linear = al.Galaxy( + redshift=1.0, + bulge=al.lp_linear.Sersic(sersic_index=1.0), + disk=al.lp_linear.Sersic(sersic_index=4.0), + ) + + tracer_linear = al.Tracer(galaxies=[lens_galaxy_linear, source_galaxy_linear]) + + fit_linear = al.FitInterferometer( + dataset=dataset, + tracer=tracer_linear, + ) + + assert fit_linear.inversion.reconstruction == pytest.approx( + np.array([0.1, 0.1, 0.2]), 1.0e-4 + ) + assert fit_linear.linear_light_profile_intensity_dict[ + lens_galaxy_linear.light + ] == pytest.approx(0.1, 1.0e-2) + assert fit_linear.linear_light_profile_intensity_dict[ + source_galaxy_linear.bulge + ] == pytest.approx(0.1, 1.0e-2) + assert fit_linear.linear_light_profile_intensity_dict[ + source_galaxy_linear.disk + ] == pytest.approx(0.2, 1.0e-2) + assert fit.log_likelihood == pytest.approx(fit_linear.log_likelihood) + + lens_galaxy_image = lens_galaxy.image_2d_from(grid=dataset.grids.lp) + + assert fit_linear.galaxy_image_dict[lens_galaxy_linear] == pytest.approx( + lens_galaxy_image.array, 1.0e-4 + ) + + traced_grid_2d_list = tracer.traced_grid_2d_list_from(grid=dataset.grids.lp) + + source_galaxy_image = source_galaxy.image_2d_from(grid=traced_grid_2d_list[1]) + + assert fit_linear.galaxy_image_dict[source_galaxy_linear] == pytest.approx( + source_galaxy_image.array, 1.0e-4 + ) + + lens_galaxy_visibilities = lens_galaxy.visibilities_from( + grid=dataset.grids.lp, transformer=dataset.transformer + ) + + assert fit_linear.galaxy_model_visibilities_dict[ + lens_galaxy_linear + ].array == pytest.approx(lens_galaxy_visibilities.array, 1.0e-4) + + source_galaxy_visibilities = source_galaxy.visibilities_from( + grid=traced_grid_2d_list[1], transformer=dataset.transformer + ) + + assert fit_linear.galaxy_model_visibilities_dict[ + source_galaxy_linear + ].array == pytest.approx(source_galaxy_visibilities.array, 1.0e-4) + + +def test__simulate_interferometer_data_and_fit__linear_light_profiles_and_pixelization(): + grid = al.Grid2D.uniform( + shape_native=(51, 51), pixel_scales=0.1, over_sample_size=1 + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(centre=(0.1, 0.1), intensity=100.0), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), + ) + + source_galaxy = al.Galaxy( + redshift=1.0, + bulge=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.1, sersic_index=1.0), + disk=al.lp.Sersic(centre=(0.1, 0.1), intensity=0.2, sersic_index=4.0), + ) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + simulator = al.SimulatorInterferometer( + uv_wavelengths=np.array( + [ + [0.04, 200.0, 0.3, 400000.0, 60000000.0], + [0.00003, 500.0, 600000.0, 0.1, 75555555], + ] + ), + transformer_class=al.TransformerDFT, + exposure_time=300.0, + noise_if_add_noise_false=1.0, + noise_sigma=None, + ) + + dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) + + lens_galaxy_linear = al.Galaxy( + redshift=0.5, + light=al.lp_linear.Sersic(centre=(0.1, 0.1)), + mass=al.mp.Isothermal(centre=(0.1, 0.1), einstein_radius=1.0), + ) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=0.01), + ) + + source_galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization) + + tracer_linear = al.Tracer(galaxies=[lens_galaxy_linear, source_galaxy_pix]) + + fit_linear = al.FitInterferometer( + dataset=dataset, + tracer=tracer_linear, + ) + + assert fit_linear.inversion.reconstruction == pytest.approx( + np.array( + [ + 101.76664331, + 0.49639672, + 0.49531196, + 0.49854243, + 0.44661417, + 0.44782337, + 0.44844437, + 0.39942579, + 0.40320996, + 0.40104302, + ] + ), + 1.0e-2, + ) + assert fit_linear.figure_of_merit == pytest.approx(-29.223696823166, 1.0e-4) diff --git a/test_autolens/interferometer/test_simulator.py b/test_autolens/interferometer/test_simulator.py index 0faaa82ba..f907e2c3e 100644 --- a/test_autolens/interferometer/test_simulator.py +++ b/test_autolens/interferometer/test_simulator.py @@ -1,109 +1,109 @@ -import autolens as al -import numpy as np -import pytest - - -def test__from_tracer__same_as_tracer_input(): - grid = al.Grid2D.uniform(shape_native=(20, 20), pixel_scales=0.05) - - lens_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic(intensity=1.0), - mass=al.mp.Isothermal(einstein_radius=1.6), - ) - - source_galaxy = al.Galaxy(redshift=1.0, light=al.lp.Sersic(intensity=0.3)) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - simulator = al.SimulatorInterferometer( - uv_wavelengths=np.ones(shape=(7, 2)), - exposure_time=10000.0, - noise_sigma=0.1, - noise_seed=1, - ) - - dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) - - interferometer_via_image = simulator.via_image_from( - image=tracer.image_2d_from(grid=grid) - ) - - assert (dataset.data == interferometer_via_image.data.array).all() - assert (dataset.uv_wavelengths == interferometer_via_image.uv_wavelengths).all() - assert (dataset.noise_map == interferometer_via_image.noise_map).all() - - -def test__via_deflections_and_galaxies_from__same_as_calculation_using_tracer(): - grid = al.Grid2D.uniform( - shape_native=(20, 20), pixel_scales=0.05, over_sample_size=1 - ) - - lens_galaxy = al.Galaxy(redshift=0.5, mass=al.mp.Isothermal(einstein_radius=1.6)) - - source_galaxy = al.Galaxy(redshift=1.0, light=al.lp.Sersic(intensity=0.3)) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - simulator = al.SimulatorInterferometer( - uv_wavelengths=np.ones(shape=(7, 2)), - exposure_time=10000.0, - noise_sigma=0.1, - noise_seed=1, - ) - - dataset = simulator.via_deflections_and_galaxies_from( - deflections=tracer.deflections_yx_2d_from(grid=grid), - galaxies=[source_galaxy], - ) - - interferometer_via_image = simulator.via_image_from( - image=tracer.image_2d_from(grid=grid) - ) - - assert (dataset.data == interferometer_via_image.data.array).all() - assert (interferometer_via_image.uv_wavelengths == dataset.uv_wavelengths).all() - assert (dataset.noise_map == interferometer_via_image.noise_map).all() - - -def test__simulate_interferometer_from_lens__source_galaxy__compare_to_interferometer(): - lens_galaxy = al.Galaxy( - redshift=0.5, - mass=al.mp.Isothermal( - centre=(0.0, 0.0), einstein_radius=1.6, ell_comps=(0.17647, 0.0) - ), - ) - - source_galaxy = al.Galaxy( - redshift=0.5, - light=al.lp.Sersic( - centre=(0.1, 0.1), - ell_comps=(0.096225, -0.055555), - intensity=0.3, - effective_radius=1.0, - sersic_index=2.5, - ), - ) - - grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.05) - - simulator = al.SimulatorInterferometer( - uv_wavelengths=np.ones(shape=(7, 2)), - exposure_time=10000.0, - noise_sigma=0.1, - noise_seed=1, - ) - - dataset = simulator.via_galaxies_from( - galaxies=[lens_galaxy, source_galaxy], grid=grid - ) - - tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) - - interferometer_via_image = simulator.via_image_from( - image=tracer.image_2d_from(grid=grid) - ) - - assert dataset.data == pytest.approx(interferometer_via_image.data.array, 1.0e-4) - assert (dataset.uv_wavelengths == interferometer_via_image.uv_wavelengths).all() - assert (interferometer_via_image.noise_map == dataset.noise_map).all() +import autolens as al +import numpy as np +import pytest + + +def test__from_tracer__same_as_tracer_input(): + grid = al.Grid2D.uniform(shape_native=(20, 20), pixel_scales=0.05) + + lens_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic(intensity=1.0), + mass=al.mp.Isothermal(einstein_radius=1.6), + ) + + source_galaxy = al.Galaxy(redshift=1.0, light=al.lp.Sersic(intensity=0.3)) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + simulator = al.SimulatorInterferometer( + uv_wavelengths=np.ones(shape=(7, 2)), + exposure_time=10000.0, + noise_sigma=0.1, + noise_seed=1, + ) + + dataset = simulator.via_tracer_from(tracer=tracer, grid=grid) + + interferometer_via_image = simulator.via_image_from( + image=tracer.image_2d_from(grid=grid) + ) + + assert (dataset.data == interferometer_via_image.data.array).all() + assert (dataset.uv_wavelengths == interferometer_via_image.uv_wavelengths).all() + assert (dataset.noise_map == interferometer_via_image.noise_map).all() + + +def test__via_deflections_and_galaxies_from__same_as_calculation_using_tracer(): + grid = al.Grid2D.uniform( + shape_native=(20, 20), pixel_scales=0.05, over_sample_size=1 + ) + + lens_galaxy = al.Galaxy(redshift=0.5, mass=al.mp.Isothermal(einstein_radius=1.6)) + + source_galaxy = al.Galaxy(redshift=1.0, light=al.lp.Sersic(intensity=0.3)) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + simulator = al.SimulatorInterferometer( + uv_wavelengths=np.ones(shape=(7, 2)), + exposure_time=10000.0, + noise_sigma=0.1, + noise_seed=1, + ) + + dataset = simulator.via_deflections_and_galaxies_from( + deflections=tracer.deflections_yx_2d_from(grid=grid), + galaxies=[source_galaxy], + ) + + interferometer_via_image = simulator.via_image_from( + image=tracer.image_2d_from(grid=grid) + ) + + assert (dataset.data == interferometer_via_image.data.array).all() + assert (interferometer_via_image.uv_wavelengths == dataset.uv_wavelengths).all() + assert (dataset.noise_map == interferometer_via_image.noise_map).all() + + +def test__simulate_interferometer_from_lens__source_galaxy__compare_to_interferometer(): + lens_galaxy = al.Galaxy( + redshift=0.5, + mass=al.mp.Isothermal( + centre=(0.0, 0.0), einstein_radius=1.6, ell_comps=(0.17647, 0.0) + ), + ) + + source_galaxy = al.Galaxy( + redshift=0.5, + light=al.lp.Sersic( + centre=(0.1, 0.1), + ell_comps=(0.096225, -0.055555), + intensity=0.3, + effective_radius=1.0, + sersic_index=2.5, + ), + ) + + grid = al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.05) + + simulator = al.SimulatorInterferometer( + uv_wavelengths=np.ones(shape=(7, 2)), + exposure_time=10000.0, + noise_sigma=0.1, + noise_seed=1, + ) + + dataset = simulator.via_galaxies_from( + galaxies=[lens_galaxy, source_galaxy], grid=grid + ) + + tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + + interferometer_via_image = simulator.via_image_from( + image=tracer.image_2d_from(grid=grid) + ) + + assert dataset.data == pytest.approx(interferometer_via_image.data.array, 1.0e-4) + assert (dataset.uv_wavelengths == interferometer_via_image.uv_wavelengths).all() + assert (interferometer_via_image.noise_map == dataset.noise_map).all() diff --git a/test_autolens/lens/test_operate.py b/test_autolens/lens/test_operate.py index c355d6950..4f10fd185 100644 --- a/test_autolens/lens/test_operate.py +++ b/test_autolens/lens/test_operate.py @@ -1,201 +1,201 @@ -import numpy as np -import pytest -from pathlib import Path - -import autogalaxy as ag -import autolens as al - -test_path = Path(__file__).resolve().parent / "files" - - -def test__operate_image__blurred_images_2d_via_psf_from__for_tracer_gives_list_of_planes( - grid_2d_7x7, blurring_grid_2d_7x7, psf_3x3 -): - g0 = al.Galaxy( - redshift=0.5, - light_profile=al.lp.Sersic(intensity=1.0), - mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), - ) - g1 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=2.0)) - - blurred_image_0 = g0.blurred_image_2d_from( - grid=grid_2d_7x7, psf=psf_3x3, blurring_grid=blurring_grid_2d_7x7 - ) - - source_grid_2d_7x7 = g0.traced_grid_2d_from(grid=grid_2d_7x7) - source_blurring_grid_2d_7x7 = g0.traced_grid_2d_from(grid=blurring_grid_2d_7x7) - - blurred_image_1 = g1.blurred_image_2d_from( - grid=source_grid_2d_7x7, psf=psf_3x3, blurring_grid=source_blurring_grid_2d_7x7 - ) - - tracer = al.Tracer(galaxies=[g0, g1], cosmology=al.cosmo.Planck15()) - - blurred_image = tracer.blurred_image_2d_from( - grid=grid_2d_7x7, psf=psf_3x3, blurring_grid=blurring_grid_2d_7x7 - ) - - assert blurred_image.native == pytest.approx( - blurred_image_0.native.array + blurred_image_1.native.array, 1.0e-4 - ) - - blurred_image_list = tracer.blurred_image_2d_list_from( - grid=grid_2d_7x7, psf=psf_3x3, blurring_grid=blurring_grid_2d_7x7 - ) - - assert (blurred_image_list[0].slim == blurred_image_0.slim).all() - assert (blurred_image_list[1].slim == blurred_image_1.slim).all() - - assert (blurred_image_list[0].native == blurred_image_0.native).all() - assert (blurred_image_list[1].native == blurred_image_1.native).all() - - -def test__operate_image__visibilities_of_planes_from_grid_and_transformer( - grid_2d_7x7, transformer_7x7_7 -): - g0 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=1.0)) - g1 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=2.0)) - - visibilities_0 = g0.visibilities_from( - grid=grid_2d_7x7, transformer=transformer_7x7_7 - ) - - visibilities_1 = g1.visibilities_from( - grid=grid_2d_7x7, transformer=transformer_7x7_7 - ) - - tracer = al.Tracer(galaxies=[g0, g1], cosmology=al.cosmo.Planck15()) - - visibilities = tracer.visibilities_list_from( - grid=grid_2d_7x7, transformer=transformer_7x7_7 - ) - - assert (visibilities[0] == visibilities_0).all() - assert (visibilities[1] == visibilities_1).all() - - -def test__operate_image__galaxy_blurred_image_2d_dict_from( - grid_2d_7x7, blurring_grid_2d_7x7, psf_3x3 -): - g0 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=1.0)) - g1 = al.Galaxy( - redshift=0.5, - mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), - light_profile=al.lp.Sersic(intensity=2.0), - ) - - g2 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=3.0)) - - g3 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=5.0)) - - g0_blurred_image = g0.blurred_image_2d_from( - grid=grid_2d_7x7, - psf=psf_3x3, - blurring_grid=blurring_grid_2d_7x7, - ) - - g1_blurred_image = g1.blurred_image_2d_from( - grid=grid_2d_7x7, - psf=psf_3x3, - blurring_grid=blurring_grid_2d_7x7, - ) - - g2_blurred_image = g2.blurred_image_2d_from( - grid=grid_2d_7x7, - psf=psf_3x3, - blurring_grid=blurring_grid_2d_7x7, - ) - - source_grid_2d_7x7 = g1.traced_grid_2d_from(grid=grid_2d_7x7) - source_blurring_grid_2d_7x7 = g1.traced_grid_2d_from(grid=blurring_grid_2d_7x7) - - g3_blurred_image = g3.blurred_image_2d_from( - grid=source_grid_2d_7x7, - psf=psf_3x3, - blurring_grid=source_blurring_grid_2d_7x7, - ) - - tracer = al.Tracer(galaxies=[g3, g1, g0, g2], cosmology=al.cosmo.Planck15()) - - blurred_image_dict = tracer.galaxy_blurred_image_2d_dict_from( - grid=grid_2d_7x7, - psf=psf_3x3, - blurring_grid=blurring_grid_2d_7x7, - ) - - assert blurred_image_dict[g0].slim == pytest.approx( - g0_blurred_image.slim.array, 1.0e-4 - ) - assert blurred_image_dict[g1].slim == pytest.approx( - g1_blurred_image.slim.array, 1.0e-4 - ) - assert blurred_image_dict[g2].slim == pytest.approx( - g2_blurred_image.slim.array, 1.0e-4 - ) - assert blurred_image_dict[g3].slim == pytest.approx( - g3_blurred_image.slim.array, 1.0e-4 - ) - - -def test__operate_image__galaxy_visibilities_dict_from_grid_and_transformer( - grid_2d_7x7, transformer_7x7_7 -): - g0 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=1.0)) - g1 = al.Galaxy( - redshift=0.5, - mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), - light_profile=al.lp.Sersic(intensity=2.0), - ) - g2 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=3.0)) - g3 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=5.0)) - - g0_visibilities = g0.visibilities_from( - grid=grid_2d_7x7, transformer=transformer_7x7_7 - ) - g1_visibilities = g1.visibilities_from( - grid=grid_2d_7x7, transformer=transformer_7x7_7 - ) - - g2_visibilities = g2.visibilities_from( - grid=grid_2d_7x7, transformer=transformer_7x7_7 - ) - - source_grid_2d_7x7 = g1.traced_grid_2d_from(grid=grid_2d_7x7) - - g3_visibilities = g3.visibilities_from( - grid=source_grid_2d_7x7, transformer=transformer_7x7_7 - ) - - tracer = al.Tracer(galaxies=[g3, g1, g0, g2], cosmology=al.cosmo.Planck15()) - - visibilities_dict = tracer.galaxy_visibilities_dict_from( - grid=grid_2d_7x7, transformer=transformer_7x7_7 - ) - - assert (visibilities_dict[g0] == g0_visibilities).all() - assert (visibilities_dict[g1] == g1_visibilities).all() - assert (visibilities_dict[g2] == g2_visibilities).all() - assert (visibilities_dict[g3] == g3_visibilities).all() - - -def test__operate_lens__sums_individual_quantities(): - grid = al.Grid2D.uniform(shape_native=(50, 50), pixel_scales=0.15) - - sis_0 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.2) - sis_1 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.4) - sis_2 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.6) - sis_3 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.8) - - galaxy_0 = al.Galaxy(mass_profile_0=sis_0, mass_profile_1=sis_1, redshift=0.5) - galaxy_1 = al.Galaxy(mass_profile_0=sis_2, mass_profile_1=sis_3, redshift=0.5) - - tracer = al.Tracer( - galaxies=[galaxy_0, galaxy_1], - cosmology=al.cosmo.Planck15(), - ) - - einstein_mass = ag.LensCalc.from_mass_obj( - tracer - ).einstein_mass_angular_from(grid=grid) - - assert einstein_mass == pytest.approx(np.pi * 2.0**2.0, 1.0e-1) +import numpy as np +import pytest +from pathlib import Path + +import autogalaxy as ag +import autolens as al + +test_path = Path(__file__).resolve().parent / "files" + + +def test__operate_image__blurred_images_2d_via_psf_from__for_tracer_gives_list_of_planes( + grid_2d_7x7, blurring_grid_2d_7x7, psf_3x3 +): + g0 = al.Galaxy( + redshift=0.5, + light_profile=al.lp.Sersic(intensity=1.0), + mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), + ) + g1 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=2.0)) + + blurred_image_0 = g0.blurred_image_2d_from( + grid=grid_2d_7x7, psf=psf_3x3, blurring_grid=blurring_grid_2d_7x7 + ) + + source_grid_2d_7x7 = g0.traced_grid_2d_from(grid=grid_2d_7x7) + source_blurring_grid_2d_7x7 = g0.traced_grid_2d_from(grid=blurring_grid_2d_7x7) + + blurred_image_1 = g1.blurred_image_2d_from( + grid=source_grid_2d_7x7, psf=psf_3x3, blurring_grid=source_blurring_grid_2d_7x7 + ) + + tracer = al.Tracer(galaxies=[g0, g1], cosmology=al.cosmo.Planck15()) + + blurred_image = tracer.blurred_image_2d_from( + grid=grid_2d_7x7, psf=psf_3x3, blurring_grid=blurring_grid_2d_7x7 + ) + + assert blurred_image.native == pytest.approx( + blurred_image_0.native.array + blurred_image_1.native.array, 1.0e-4 + ) + + blurred_image_list = tracer.blurred_image_2d_list_from( + grid=grid_2d_7x7, psf=psf_3x3, blurring_grid=blurring_grid_2d_7x7 + ) + + assert (blurred_image_list[0].slim == blurred_image_0.slim).all() + assert (blurred_image_list[1].slim == blurred_image_1.slim).all() + + assert (blurred_image_list[0].native == blurred_image_0.native).all() + assert (blurred_image_list[1].native == blurred_image_1.native).all() + + +def test__operate_image__visibilities_of_planes_from_grid_and_transformer( + grid_2d_7x7, transformer_7x7_7 +): + g0 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=1.0)) + g1 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=2.0)) + + visibilities_0 = g0.visibilities_from( + grid=grid_2d_7x7, transformer=transformer_7x7_7 + ) + + visibilities_1 = g1.visibilities_from( + grid=grid_2d_7x7, transformer=transformer_7x7_7 + ) + + tracer = al.Tracer(galaxies=[g0, g1], cosmology=al.cosmo.Planck15()) + + visibilities = tracer.visibilities_list_from( + grid=grid_2d_7x7, transformer=transformer_7x7_7 + ) + + assert (visibilities[0] == visibilities_0).all() + assert (visibilities[1] == visibilities_1).all() + + +def test__operate_image__galaxy_blurred_image_2d_dict_from( + grid_2d_7x7, blurring_grid_2d_7x7, psf_3x3 +): + g0 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=1.0)) + g1 = al.Galaxy( + redshift=0.5, + mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), + light_profile=al.lp.Sersic(intensity=2.0), + ) + + g2 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=3.0)) + + g3 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=5.0)) + + g0_blurred_image = g0.blurred_image_2d_from( + grid=grid_2d_7x7, + psf=psf_3x3, + blurring_grid=blurring_grid_2d_7x7, + ) + + g1_blurred_image = g1.blurred_image_2d_from( + grid=grid_2d_7x7, + psf=psf_3x3, + blurring_grid=blurring_grid_2d_7x7, + ) + + g2_blurred_image = g2.blurred_image_2d_from( + grid=grid_2d_7x7, + psf=psf_3x3, + blurring_grid=blurring_grid_2d_7x7, + ) + + source_grid_2d_7x7 = g1.traced_grid_2d_from(grid=grid_2d_7x7) + source_blurring_grid_2d_7x7 = g1.traced_grid_2d_from(grid=blurring_grid_2d_7x7) + + g3_blurred_image = g3.blurred_image_2d_from( + grid=source_grid_2d_7x7, + psf=psf_3x3, + blurring_grid=source_blurring_grid_2d_7x7, + ) + + tracer = al.Tracer(galaxies=[g3, g1, g0, g2], cosmology=al.cosmo.Planck15()) + + blurred_image_dict = tracer.galaxy_blurred_image_2d_dict_from( + grid=grid_2d_7x7, + psf=psf_3x3, + blurring_grid=blurring_grid_2d_7x7, + ) + + assert blurred_image_dict[g0].slim == pytest.approx( + g0_blurred_image.slim.array, 1.0e-4 + ) + assert blurred_image_dict[g1].slim == pytest.approx( + g1_blurred_image.slim.array, 1.0e-4 + ) + assert blurred_image_dict[g2].slim == pytest.approx( + g2_blurred_image.slim.array, 1.0e-4 + ) + assert blurred_image_dict[g3].slim == pytest.approx( + g3_blurred_image.slim.array, 1.0e-4 + ) + + +def test__operate_image__galaxy_visibilities_dict_from_grid_and_transformer( + grid_2d_7x7, transformer_7x7_7 +): + g0 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=1.0)) + g1 = al.Galaxy( + redshift=0.5, + mass_profile=al.mp.IsothermalSph(einstein_radius=1.0), + light_profile=al.lp.Sersic(intensity=2.0), + ) + g2 = al.Galaxy(redshift=0.5, light_profile=al.lp.Sersic(intensity=3.0)) + g3 = al.Galaxy(redshift=1.0, light_profile=al.lp.Sersic(intensity=5.0)) + + g0_visibilities = g0.visibilities_from( + grid=grid_2d_7x7, transformer=transformer_7x7_7 + ) + g1_visibilities = g1.visibilities_from( + grid=grid_2d_7x7, transformer=transformer_7x7_7 + ) + + g2_visibilities = g2.visibilities_from( + grid=grid_2d_7x7, transformer=transformer_7x7_7 + ) + + source_grid_2d_7x7 = g1.traced_grid_2d_from(grid=grid_2d_7x7) + + g3_visibilities = g3.visibilities_from( + grid=source_grid_2d_7x7, transformer=transformer_7x7_7 + ) + + tracer = al.Tracer(galaxies=[g3, g1, g0, g2], cosmology=al.cosmo.Planck15()) + + visibilities_dict = tracer.galaxy_visibilities_dict_from( + grid=grid_2d_7x7, transformer=transformer_7x7_7 + ) + + assert (visibilities_dict[g0] == g0_visibilities).all() + assert (visibilities_dict[g1] == g1_visibilities).all() + assert (visibilities_dict[g2] == g2_visibilities).all() + assert (visibilities_dict[g3] == g3_visibilities).all() + + +def test__operate_lens__sums_individual_quantities(): + grid = al.Grid2D.uniform(shape_native=(50, 50), pixel_scales=0.15) + + sis_0 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.2) + sis_1 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.4) + sis_2 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.6) + sis_3 = al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.8) + + galaxy_0 = al.Galaxy(mass_profile_0=sis_0, mass_profile_1=sis_1, redshift=0.5) + galaxy_1 = al.Galaxy(mass_profile_0=sis_2, mass_profile_1=sis_3, redshift=0.5) + + tracer = al.Tracer( + galaxies=[galaxy_0, galaxy_1], + cosmology=al.cosmo.Planck15(), + ) + + einstein_mass = ag.LensCalc.from_mass_obj( + tracer + ).einstein_mass_angular_from(grid=grid) + + assert einstein_mass == pytest.approx(np.pi * 2.0**2.0, 1.0e-1) diff --git a/test_autolens/lens/test_subhalo.py b/test_autolens/lens/test_subhalo.py index 395e36b6a..2e3356c2c 100644 --- a/test_autolens/lens/test_subhalo.py +++ b/test_autolens/lens/test_subhalo.py @@ -1,35 +1,35 @@ -import autofit as af -import autolens as al - - -# def test__detection_array_from(): -# samples_list = [ -# [ -# [ -# af.mock.MockSamples(log_likelihood_list=[1.0]), -# af.mock.MockSamples(log_likelihood_list=[2.0]), -# ], -# [ -# af.mock.MockSamples(log_likelihood_list=[3.0]), -# af.mock.MockSamples(log_likelihood_list=[4.0]), -# ], -# ], -# ] -# -# grid_search_result_with_subhalo = af.GridSearchResult( -# lower_limits_lists=[[1.0, 2.0], [3.0, 4.0]], -# samples=samples_list, -# grid_priors=[[1, 2], [3, 4]], -# ) -# -# subhalo_result = al.subhalo.SubhaloGridSearchResult( -# subhalo_grid_search_result=grid_search_result_with_subhalo, -# fit_imaging_no_subhalo=None, -# samples_no_subhalo=None, -# ) -# -# detection_array = subhalo_result.detection_array_from( -# use_log_evidences=False, relative_to_no_subhalo=False, remove_zeros=False -# ) -# -# print(detection_array) +import autofit as af +import autolens as al + + +# def test__detection_array_from(): +# samples_list = [ +# [ +# [ +# af.mock.MockSamples(log_likelihood_list=[1.0]), +# af.mock.MockSamples(log_likelihood_list=[2.0]), +# ], +# [ +# af.mock.MockSamples(log_likelihood_list=[3.0]), +# af.mock.MockSamples(log_likelihood_list=[4.0]), +# ], +# ], +# ] +# +# grid_search_result_with_subhalo = af.GridSearchResult( +# lower_limits_lists=[[1.0, 2.0], [3.0, 4.0]], +# samples=samples_list, +# grid_priors=[[1, 2], [3, 4]], +# ) +# +# subhalo_result = al.subhalo.SubhaloGridSearchResult( +# subhalo_grid_search_result=grid_search_result_with_subhalo, +# fit_imaging_no_subhalo=None, +# samples_no_subhalo=None, +# ) +# +# detection_array = subhalo_result.detection_array_from( +# use_log_evidences=False, relative_to_no_subhalo=False, remove_zeros=False +# ) +# +# print(detection_array) diff --git a/test_autolens/lens/test_to_inversion.py b/test_autolens/lens/test_to_inversion.py index b54c81c31..01d2e2ba6 100644 --- a/test_autolens/lens/test_to_inversion.py +++ b/test_autolens/lens/test_to_inversion.py @@ -1,585 +1,585 @@ -import numpy as np -import pytest -from pathlib import Path - -import autolens as al - -test_path = Path(__file__).resolve().parent / "files" - - -def test__lp_linear_func_galaxy_dict_from(masked_imaging_7x7): - # TODO : use pytest.parameterize - - galaxy_no_pix = al.Galaxy(redshift=0.5) - - tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_no_pix]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, - tracer=tracer, - ) - - lp_linear_func_galaxy_dict = tracer_to_inversion.lp_linear_func_list_galaxy_dict - - assert lp_linear_func_galaxy_dict == {} - - lp_linear_0 = al.lp_linear.LightProfileLinear() - lp_linear_1 = al.lp_linear.LightProfileLinear() - lp_linear_2 = al.lp_linear.LightProfileLinear() - - galaxy_no_linear = al.Galaxy(redshift=0.5) - galaxy_linear_0 = al.Galaxy( - redshift=0.5, lp_linear=lp_linear_0, mass=al.mp.IsothermalSph() - ) - - galaxy_linear_1 = al.Galaxy( - redshift=1.0, lp_linear=lp_linear_1, mass=al.mp.IsothermalSph() - ) - galaxy_linear_2 = al.Galaxy(redshift=2.0, lp_linear=lp_linear_2) - - tracer = al.Tracer( - galaxies=[galaxy_no_linear, galaxy_linear_0, galaxy_linear_1, galaxy_linear_2] - ) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - lp_linear_func_galaxy_dict = tracer_to_inversion.lp_linear_func_list_galaxy_dict - - lp_linear_func_list = list(lp_linear_func_galaxy_dict.keys()) - - assert lp_linear_func_galaxy_dict[lp_linear_func_list[0]] == galaxy_linear_0 - assert lp_linear_func_galaxy_dict[lp_linear_func_list[1]] == galaxy_linear_1 - assert lp_linear_func_galaxy_dict[lp_linear_func_list[2]] == galaxy_linear_2 - - assert lp_linear_func_list[0].light_profile_list[0] == lp_linear_0 - assert lp_linear_func_list[1].light_profile_list[0] == lp_linear_1 - assert lp_linear_func_list[2].light_profile_list[0] == lp_linear_2 - - traced_grid_list = tracer.traced_grid_2d_list_from(grid=masked_imaging_7x7.grids.lp) - - assert lp_linear_func_list[0].grid == pytest.approx( - masked_imaging_7x7.grids.lp, 1.0e-4 - ) - assert lp_linear_func_list[1].grid == pytest.approx( - traced_grid_list[1].array, 1.0e-4 - ) - assert lp_linear_func_list[2].grid == pytest.approx( - traced_grid_list[2].array, 1.0e-4 - ) - - lp_linear_3 = al.lp_linear.LightProfileLinear() - lp_linear_4 = al.lp_linear.LightProfileLinear() - - basis_0 = al.lp_basis.Basis(profile_list=[lp_linear_0, lp_linear_1]) - - galaxy_linear_0 = al.Galaxy(redshift=0.5, bulge=basis_0, mass=al.mp.IsothermalSph()) - - galaxy_linear_1 = al.Galaxy(redshift=1.0, mass=al.mp.IsothermalSph()) - - galaxy_linear_2 = al.Galaxy(redshift=2.0, lp_linear=lp_linear_2) - - basis_1 = al.lp_basis.Basis(profile_list=[lp_linear_3, lp_linear_4]) - - galaxy_linear_3 = al.Galaxy(redshift=2.0, bulge=basis_1) - - tracer = al.Tracer( - galaxies=[ - galaxy_no_linear, - galaxy_linear_0, - galaxy_linear_1, - galaxy_linear_2, - galaxy_linear_3, - ] - ) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, - tracer=tracer, - ) - - lp_linear_func_galaxy_dict = tracer_to_inversion.lp_linear_func_list_galaxy_dict - - lp_linear_func_list = list(lp_linear_func_galaxy_dict.keys()) - - assert lp_linear_func_galaxy_dict[lp_linear_func_list[0]] == galaxy_linear_0 - assert lp_linear_func_galaxy_dict[lp_linear_func_list[1]] == galaxy_linear_2 - assert lp_linear_func_galaxy_dict[lp_linear_func_list[2]] == galaxy_linear_3 - - assert lp_linear_func_list[0].light_profile_list[0] == lp_linear_0 - assert lp_linear_func_list[0].light_profile_list[1] == lp_linear_1 - assert lp_linear_func_list[1].light_profile_list[0] == lp_linear_2 - assert lp_linear_func_list[2].light_profile_list[0] == lp_linear_3 - - -def test__cls_pg_list_from(masked_imaging_7x7, grid_2d_7x7): - mesh_0 = al.mesh.RectangularUniform(shape=(3, 3)) - - pixelization_0 = al.Pixelization(mesh=mesh_0) - - galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization_0) - galaxy_no_pix = al.Galaxy(redshift=0.5) - - tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_pix]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - pixelization_list = tracer_to_inversion.cls_pg_list_from(cls=al.Pixelization) - - assert pixelization_list[0] == [] - assert pixelization_list[1][0].mesh.pixels == 9 - - mesh_1 = al.mesh.RectangularUniform(shape=(4, 3)) - - pixelization_1 = al.Pixelization(mesh=mesh_1) - - mesh_2 = al.mesh.RectangularUniform(shape=(4, 4)) - - pixelization_2 = al.Pixelization(mesh=mesh_2) - - galaxy_pix_0 = al.Galaxy(redshift=0.5, pixelization=pixelization_0) - - galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization_1) - - galaxy_pix_2 = al.Galaxy(redshift=1.0, pixelization=pixelization_2) - - tracer = al.Tracer(galaxies=[galaxy_pix_0, galaxy_pix_1, galaxy_pix_2]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - pixelization_list = tracer_to_inversion.cls_pg_list_from(cls=al.Pixelization) - - assert pixelization_list[0][0].mesh.pixels == 9 - assert pixelization_list[1][0].mesh.pixels == 12 - assert pixelization_list[1][1].mesh.pixels == 16 - - galaxy_no_pix = al.Galaxy(redshift=0.5) - - tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_no_pix]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - pixelization_list = tracer_to_inversion.cls_pg_list_from(cls=al.Pixelization) - - assert pixelization_list == [[]] - - -def test__adapt_galaxy_image_pg_list(masked_imaging_7x7, grid_2d_7x7): - gal = al.Galaxy(redshift=0.5) - - tracer = al.Tracer(galaxies=[gal, gal]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[]] - - pixelization = al.Pixelization( - mesh=al.m.MockMesh(), regularization=al.m.MockRegularization() - ) - - gal_pix = al.Galaxy(redshift=0.5, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[gal_pix, gal_pix]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[None, None]] - - gal_pix = al.Galaxy(redshift=0.5, pixelization=pixelization) - - adapt_images = al.AdaptImages(galaxy_image_dict={gal_pix: 1}) - - tracer = al.Tracer(galaxies=[gal_pix, gal]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images - ) - - assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[1]] - - gal0 = al.Galaxy(redshift=0.25) - gal1 = al.Galaxy(redshift=0.75) - gal2 = al.Galaxy(redshift=1.5) - - gal_pix0 = al.Galaxy(redshift=0.5, pixelization=pixelization) - - gal_pix1 = al.Galaxy(redshift=2.0, pixelization=pixelization) - - gal_pix2 = al.Galaxy(redshift=2.0, pixelization=pixelization) - - adapt_images = al.AdaptImages( - galaxy_image_dict={gal_pix0: 1, gal_pix1: 2, gal_pix2: 3} - ) - - tracer = al.Tracer(galaxies=[gal0, gal1, gal2, gal_pix0, gal_pix1, gal_pix2]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images - ) - - assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[], [1], [], [], [2, 3]] - - -def test__image_plane_mesh_grid_pg_list(masked_imaging_7x7): - # Test Correct - - image_plane_mesh_grid_0 = np.array([[1.0, 1.0]]) - - galaxy_pix = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) - galaxy_no_pix = al.Galaxy(redshift=0.5) - - adapt_images = al.AdaptImages( - galaxy_image_dict={galaxy_pix: 2}, - galaxy_image_plane_mesh_grid_dict={galaxy_pix: image_plane_mesh_grid_0}, - ) - - tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_pix]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, - tracer=tracer, - adapt_images=adapt_images, - ) - - mesh_grids = tracer_to_inversion.image_plane_mesh_grid_pg_list - - assert mesh_grids[0] == None - assert (mesh_grids[1] == np.array([[1.0, 1.0]])).all() - - # Test for extra galaxies - - galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) - - image_plane_mesh_grid_1 = np.array([[2.0, 2.0]]) - - galaxy_pix_1 = al.Galaxy(redshift=2.0, pixelization=al.m.MockPixelization()) - - galaxy_no_pix_0 = al.Galaxy(redshift=0.25) - galaxy_no_pix_1 = al.Galaxy(redshift=0.5) - galaxy_no_pix_2 = al.Galaxy(redshift=1.5) - - adapt_images = al.AdaptImages( - galaxy_image_dict={galaxy_pix_0: 2, galaxy_pix_1: 3}, - galaxy_image_plane_mesh_grid_dict={ - galaxy_pix_0: image_plane_mesh_grid_0, - galaxy_pix_1: image_plane_mesh_grid_1, - }, - ) - - tracer = al.Tracer( - galaxies=[ - galaxy_pix_0, - galaxy_pix_1, - galaxy_no_pix_0, - galaxy_no_pix_1, - galaxy_no_pix_2, - ] - ) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, - tracer=tracer, - adapt_images=adapt_images, - ) - - mesh_grids = tracer_to_inversion.image_plane_mesh_grid_pg_list - - assert mesh_grids[0] == None - assert mesh_grids[1] == None - assert (mesh_grids[2] == np.array([[1.0, 1.0]])).all() - assert mesh_grids[3] == None - assert (mesh_grids[4] == np.array([[2.0, 2.0]])).all() - - -def test__traced_mesh_grid_pg_list(masked_imaging_7x7): - # Test Multi plane - - galaxy_no_pix = al.Galaxy( - redshift=0.5, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.5), - ) - - image_plane_mesh_grid_0 = al.Grid2D.no_mask( - values=[[[1.0, 0.0]]], pixel_scales=(1.0, 1.0) - ) - - galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) - - image_plane_mesh_grid_1 = al.Grid2D.no_mask( - values=[[[2.0, 0.0]]], pixel_scales=(1.0, 1.0) - ) - - galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) - - tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_pix_0, galaxy_pix_1]) - - adapt_images = al.AdaptImages( - galaxy_image_dict={galaxy_pix_0: 2, galaxy_pix_1: 3}, - galaxy_image_plane_mesh_grid_dict={ - galaxy_pix_0: image_plane_mesh_grid_0, - galaxy_pix_1: image_plane_mesh_grid_1, - }, - ) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images - ) - - traced_mesh_grids_list_of_planes = tracer_to_inversion.traced_mesh_grid_pg_list - - assert traced_mesh_grids_list_of_planes[0] == None - assert traced_mesh_grids_list_of_planes[1][0] == pytest.approx( - np.array([[1.0 - 0.5, 0.0]]), 1.0e-4 - ) - assert traced_mesh_grids_list_of_planes[1][1] == pytest.approx( - np.array([[2.0 - 0.5, 0.0]]), 1.0e-4 - ) - - # Test Extra Galaxies - - galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) - galaxy_pix_1 = al.Galaxy(redshift=2.0, pixelization=al.m.MockPixelization()) - - adapt_images = al.AdaptImages( - galaxy_image_dict={galaxy_pix_0: 2, galaxy_pix_1: 3}, - galaxy_image_plane_mesh_grid_dict={ - galaxy_pix_0: image_plane_mesh_grid_0, - galaxy_pix_1: image_plane_mesh_grid_1, - }, - ) - - galaxy_no_pix_0 = al.Galaxy( - redshift=0.25, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.5), - ) - galaxy_no_pix_1 = al.Galaxy(redshift=0.5) - galaxy_no_pix_2 = al.Galaxy(redshift=1.5) - - tracer = al.Tracer( - galaxies=[ - galaxy_pix_0, - galaxy_pix_1, - galaxy_no_pix_0, - galaxy_no_pix_1, - galaxy_no_pix_2, - ] - ) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images - ) - - traced_mesh_grids_list_of_planes = tracer_to_inversion.traced_mesh_grid_pg_list - - traced_grid_pix_0 = tracer.traced_grid_2d_list_from( - grid=al.Grid2DIrregular(values=[[1.0, 0.0]]) - )[2] - traced_grid_pix_1 = tracer.traced_grid_2d_list_from( - grid=al.Grid2DIrregular(values=[[2.0, 0.0]]) - )[4] - - assert traced_mesh_grids_list_of_planes[0] == None - assert traced_mesh_grids_list_of_planes[1] == None - assert (traced_mesh_grids_list_of_planes[2][0] == traced_grid_pix_0).all() - assert traced_mesh_grids_list_of_planes[3] == None - assert (traced_mesh_grids_list_of_planes[4][0] == traced_grid_pix_1).all() - - -def test__mapper_galaxy_dict(masked_imaging_7x7): - galaxy_no_pix = al.Galaxy(redshift=0.5) - - tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_no_pix]) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - mapper_galaxy_dict = tracer_to_inversion.mapper_galaxy_dict - assert mapper_galaxy_dict == {} - - galaxy_no_pix = al.Galaxy(redshift=0.5) - - pixelization_0 = al.m.MockPixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)) - ) - - galaxy_pix_0 = al.Galaxy(redshift=0.5, pixelization=pixelization_0) - - pixelization_1 = al.m.MockPixelization( - mesh=al.mesh.RectangularUniform(shape=(4, 3)) - ) - - galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization_1) - - pixelization_2 = al.m.MockPixelization( - mesh=al.mesh.RectangularUniform(shape=(4, 4)) - ) - - galaxy_pix_2 = al.Galaxy(redshift=1.0, pixelization=pixelization_2) - - tracer = al.Tracer( - galaxies=[galaxy_no_pix, galaxy_pix_0, galaxy_pix_1, galaxy_pix_2] - ) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - mapper_galaxy_dict = tracer_to_inversion.mapper_galaxy_dict - - mapper_list = list(mapper_galaxy_dict.keys()) - - assert mapper_galaxy_dict[mapper_list[0]] == galaxy_pix_0 - assert mapper_galaxy_dict[mapper_list[1]] == galaxy_pix_1 - assert mapper_galaxy_dict[mapper_list[2]] == galaxy_pix_2 - - assert mapper_list[0].pixels == 9 - assert mapper_list[1].pixels == 12 - assert mapper_list[2].pixels == 16 - - galaxy_no_pix_0 = al.Galaxy( - redshift=0.25, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.5), - ) - galaxy_no_pix_1 = al.Galaxy(redshift=0.5) - galaxy_no_pix_2 = al.Galaxy(redshift=1.5) - - galaxy_pix_0 = al.Galaxy(redshift=0.75, pixelization=pixelization_0) - galaxy_pix_1 = al.Galaxy(redshift=2.0, pixelization=pixelization_1) - - tracer = al.Tracer( - galaxies=[ - galaxy_no_pix_0, - galaxy_no_pix_1, - galaxy_no_pix_2, - galaxy_pix_0, - galaxy_pix_1, - ] - ) - - tracer_to_inversion = al.TracerToInversion( - dataset=masked_imaging_7x7, tracer=tracer - ) - - mapper_galaxy_dict = tracer_to_inversion.mapper_galaxy_dict - - mapper_list = list(mapper_galaxy_dict.keys()) - - assert mapper_galaxy_dict[mapper_list[0]] == galaxy_pix_0 - assert mapper_galaxy_dict[mapper_list[1]] == galaxy_pix_1 - - assert mapper_galaxy_dict[mapper_list[0]] == galaxy_pix_0 - assert mapper_galaxy_dict[mapper_list[1]] == galaxy_pix_1 - - -def test__inversion_imaging_from(grid_2d_7x7, masked_imaging_7x7): - grids = al.GridsInterface( - lp=masked_imaging_7x7.grids.lp, - pixelization=masked_imaging_7x7.grids.pixelization, - blurring=masked_imaging_7x7.grids.blurring, - border_relocator=masked_imaging_7x7.grids.border_relocator, - ) - - dataset = al.DatasetInterface( - data=masked_imaging_7x7.data, - noise_map=masked_imaging_7x7.noise_map, - grids=grids, - psf=masked_imaging_7x7.psf, - ) - - g_linear = al.Galaxy( - redshift=0.5, light_linear=al.lp_linear.Sersic(centre=(0.05, 0.05)) - ) - - tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g_linear]) - - tracer_to_inversion = al.TracerToInversion( - dataset=dataset, - tracer=tracer, - ) - - inversion = tracer_to_inversion.inversion - - assert inversion.reconstruction[0] == pytest.approx(0.186868464426, 1.0e-2) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(3, 3)), - regularization=al.reg.Constant(coefficient=0.0), - ) - - g0 = al.Galaxy(redshift=0.5, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g0]) - - tracer_to_inversion = al.TracerToInversion( - dataset=dataset, - tracer=tracer, - ) - - inversion = tracer_to_inversion.inversion - - assert inversion.mapped_reconstructed_operated_data == pytest.approx( - masked_imaging_7x7.data, 1.0e-2 - ) - - -def test__inversion_interferometer_from(grid_2d_7x7, interferometer_7): - interferometer_7.data = al.Visibilities.ones(shape_slim=(7,)) - - grids = al.GridsInterface( - lp=interferometer_7.grids.lp, - pixelization=interferometer_7.grids.pixelization, - blurring=interferometer_7.grids.blurring, - border_relocator=interferometer_7.grids.border_relocator, - ) - - dataset = al.DatasetInterface( - data=interferometer_7.data, - noise_map=interferometer_7.noise_map, - grids=grids, - transformer=interferometer_7.transformer, - ) - - g_linear = al.Galaxy( - redshift=0.5, light_linear=al.lp_linear.Sersic(centre=(0.05, 0.05)) - ) - - tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g_linear]) - - tracer_to_inversion = al.TracerToInversion( - dataset=dataset, - tracer=tracer, - ) - - inversion = tracer_to_inversion.inversion - - assert inversion.reconstruction[0] == pytest.approx(0.0412484695, 1.0e-5) - - pixelization = al.Pixelization( - mesh=al.mesh.RectangularUniform(shape=(7, 7)), - regularization=al.reg.Constant(coefficient=0.0), - ) - - g0 = al.Galaxy(redshift=0.5, pixelization=pixelization) - - tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g0]) - - tracer_to_inversion = al.TracerToInversion( - dataset=dataset, - tracer=tracer, - ) - - inversion = tracer_to_inversion.inversion - - assert inversion.reconstruction[0] == pytest.approx(-0.095834752881, 1.0e-4) +import numpy as np +import pytest +from pathlib import Path + +import autolens as al + +test_path = Path(__file__).resolve().parent / "files" + + +def test__lp_linear_func_galaxy_dict_from(masked_imaging_7x7): + # TODO : use pytest.parameterize + + galaxy_no_pix = al.Galaxy(redshift=0.5) + + tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_no_pix]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, + tracer=tracer, + ) + + lp_linear_func_galaxy_dict = tracer_to_inversion.lp_linear_func_list_galaxy_dict + + assert lp_linear_func_galaxy_dict == {} + + lp_linear_0 = al.lp_linear.LightProfileLinear() + lp_linear_1 = al.lp_linear.LightProfileLinear() + lp_linear_2 = al.lp_linear.LightProfileLinear() + + galaxy_no_linear = al.Galaxy(redshift=0.5) + galaxy_linear_0 = al.Galaxy( + redshift=0.5, lp_linear=lp_linear_0, mass=al.mp.IsothermalSph() + ) + + galaxy_linear_1 = al.Galaxy( + redshift=1.0, lp_linear=lp_linear_1, mass=al.mp.IsothermalSph() + ) + galaxy_linear_2 = al.Galaxy(redshift=2.0, lp_linear=lp_linear_2) + + tracer = al.Tracer( + galaxies=[galaxy_no_linear, galaxy_linear_0, galaxy_linear_1, galaxy_linear_2] + ) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + lp_linear_func_galaxy_dict = tracer_to_inversion.lp_linear_func_list_galaxy_dict + + lp_linear_func_list = list(lp_linear_func_galaxy_dict.keys()) + + assert lp_linear_func_galaxy_dict[lp_linear_func_list[0]] == galaxy_linear_0 + assert lp_linear_func_galaxy_dict[lp_linear_func_list[1]] == galaxy_linear_1 + assert lp_linear_func_galaxy_dict[lp_linear_func_list[2]] == galaxy_linear_2 + + assert lp_linear_func_list[0].light_profile_list[0] == lp_linear_0 + assert lp_linear_func_list[1].light_profile_list[0] == lp_linear_1 + assert lp_linear_func_list[2].light_profile_list[0] == lp_linear_2 + + traced_grid_list = tracer.traced_grid_2d_list_from(grid=masked_imaging_7x7.grids.lp) + + assert lp_linear_func_list[0].grid == pytest.approx( + masked_imaging_7x7.grids.lp, 1.0e-4 + ) + assert lp_linear_func_list[1].grid == pytest.approx( + traced_grid_list[1].array, 1.0e-4 + ) + assert lp_linear_func_list[2].grid == pytest.approx( + traced_grid_list[2].array, 1.0e-4 + ) + + lp_linear_3 = al.lp_linear.LightProfileLinear() + lp_linear_4 = al.lp_linear.LightProfileLinear() + + basis_0 = al.lp_basis.Basis(profile_list=[lp_linear_0, lp_linear_1]) + + galaxy_linear_0 = al.Galaxy(redshift=0.5, bulge=basis_0, mass=al.mp.IsothermalSph()) + + galaxy_linear_1 = al.Galaxy(redshift=1.0, mass=al.mp.IsothermalSph()) + + galaxy_linear_2 = al.Galaxy(redshift=2.0, lp_linear=lp_linear_2) + + basis_1 = al.lp_basis.Basis(profile_list=[lp_linear_3, lp_linear_4]) + + galaxy_linear_3 = al.Galaxy(redshift=2.0, bulge=basis_1) + + tracer = al.Tracer( + galaxies=[ + galaxy_no_linear, + galaxy_linear_0, + galaxy_linear_1, + galaxy_linear_2, + galaxy_linear_3, + ] + ) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, + tracer=tracer, + ) + + lp_linear_func_galaxy_dict = tracer_to_inversion.lp_linear_func_list_galaxy_dict + + lp_linear_func_list = list(lp_linear_func_galaxy_dict.keys()) + + assert lp_linear_func_galaxy_dict[lp_linear_func_list[0]] == galaxy_linear_0 + assert lp_linear_func_galaxy_dict[lp_linear_func_list[1]] == galaxy_linear_2 + assert lp_linear_func_galaxy_dict[lp_linear_func_list[2]] == galaxy_linear_3 + + assert lp_linear_func_list[0].light_profile_list[0] == lp_linear_0 + assert lp_linear_func_list[0].light_profile_list[1] == lp_linear_1 + assert lp_linear_func_list[1].light_profile_list[0] == lp_linear_2 + assert lp_linear_func_list[2].light_profile_list[0] == lp_linear_3 + + +def test__cls_pg_list_from(masked_imaging_7x7, grid_2d_7x7): + mesh_0 = al.mesh.RectangularUniform(shape=(3, 3)) + + pixelization_0 = al.Pixelization(mesh=mesh_0) + + galaxy_pix = al.Galaxy(redshift=1.0, pixelization=pixelization_0) + galaxy_no_pix = al.Galaxy(redshift=0.5) + + tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_pix]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + pixelization_list = tracer_to_inversion.cls_pg_list_from(cls=al.Pixelization) + + assert pixelization_list[0] == [] + assert pixelization_list[1][0].mesh.pixels == 9 + + mesh_1 = al.mesh.RectangularUniform(shape=(4, 3)) + + pixelization_1 = al.Pixelization(mesh=mesh_1) + + mesh_2 = al.mesh.RectangularUniform(shape=(4, 4)) + + pixelization_2 = al.Pixelization(mesh=mesh_2) + + galaxy_pix_0 = al.Galaxy(redshift=0.5, pixelization=pixelization_0) + + galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization_1) + + galaxy_pix_2 = al.Galaxy(redshift=1.0, pixelization=pixelization_2) + + tracer = al.Tracer(galaxies=[galaxy_pix_0, galaxy_pix_1, galaxy_pix_2]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + pixelization_list = tracer_to_inversion.cls_pg_list_from(cls=al.Pixelization) + + assert pixelization_list[0][0].mesh.pixels == 9 + assert pixelization_list[1][0].mesh.pixels == 12 + assert pixelization_list[1][1].mesh.pixels == 16 + + galaxy_no_pix = al.Galaxy(redshift=0.5) + + tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_no_pix]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + pixelization_list = tracer_to_inversion.cls_pg_list_from(cls=al.Pixelization) + + assert pixelization_list == [[]] + + +def test__adapt_galaxy_image_pg_list(masked_imaging_7x7, grid_2d_7x7): + gal = al.Galaxy(redshift=0.5) + + tracer = al.Tracer(galaxies=[gal, gal]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[]] + + pixelization = al.Pixelization( + mesh=al.m.MockMesh(), regularization=al.m.MockRegularization() + ) + + gal_pix = al.Galaxy(redshift=0.5, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[gal_pix, gal_pix]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[None, None]] + + gal_pix = al.Galaxy(redshift=0.5, pixelization=pixelization) + + adapt_images = al.AdaptImages(galaxy_image_dict={gal_pix: 1}) + + tracer = al.Tracer(galaxies=[gal_pix, gal]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images + ) + + assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[1]] + + gal0 = al.Galaxy(redshift=0.25) + gal1 = al.Galaxy(redshift=0.75) + gal2 = al.Galaxy(redshift=1.5) + + gal_pix0 = al.Galaxy(redshift=0.5, pixelization=pixelization) + + gal_pix1 = al.Galaxy(redshift=2.0, pixelization=pixelization) + + gal_pix2 = al.Galaxy(redshift=2.0, pixelization=pixelization) + + adapt_images = al.AdaptImages( + galaxy_image_dict={gal_pix0: 1, gal_pix1: 2, gal_pix2: 3} + ) + + tracer = al.Tracer(galaxies=[gal0, gal1, gal2, gal_pix0, gal_pix1, gal_pix2]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images + ) + + assert tracer_to_inversion.adapt_galaxy_image_pg_list == [[], [1], [], [], [2, 3]] + + +def test__image_plane_mesh_grid_pg_list(masked_imaging_7x7): + # Test Correct + + image_plane_mesh_grid_0 = np.array([[1.0, 1.0]]) + + galaxy_pix = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) + galaxy_no_pix = al.Galaxy(redshift=0.5) + + adapt_images = al.AdaptImages( + galaxy_image_dict={galaxy_pix: 2}, + galaxy_image_plane_mesh_grid_dict={galaxy_pix: image_plane_mesh_grid_0}, + ) + + tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_pix]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, + tracer=tracer, + adapt_images=adapt_images, + ) + + mesh_grids = tracer_to_inversion.image_plane_mesh_grid_pg_list + + assert mesh_grids[0] == None + assert (mesh_grids[1] == np.array([[1.0, 1.0]])).all() + + # Test for extra galaxies + + galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) + + image_plane_mesh_grid_1 = np.array([[2.0, 2.0]]) + + galaxy_pix_1 = al.Galaxy(redshift=2.0, pixelization=al.m.MockPixelization()) + + galaxy_no_pix_0 = al.Galaxy(redshift=0.25) + galaxy_no_pix_1 = al.Galaxy(redshift=0.5) + galaxy_no_pix_2 = al.Galaxy(redshift=1.5) + + adapt_images = al.AdaptImages( + galaxy_image_dict={galaxy_pix_0: 2, galaxy_pix_1: 3}, + galaxy_image_plane_mesh_grid_dict={ + galaxy_pix_0: image_plane_mesh_grid_0, + galaxy_pix_1: image_plane_mesh_grid_1, + }, + ) + + tracer = al.Tracer( + galaxies=[ + galaxy_pix_0, + galaxy_pix_1, + galaxy_no_pix_0, + galaxy_no_pix_1, + galaxy_no_pix_2, + ] + ) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, + tracer=tracer, + adapt_images=adapt_images, + ) + + mesh_grids = tracer_to_inversion.image_plane_mesh_grid_pg_list + + assert mesh_grids[0] == None + assert mesh_grids[1] == None + assert (mesh_grids[2] == np.array([[1.0, 1.0]])).all() + assert mesh_grids[3] == None + assert (mesh_grids[4] == np.array([[2.0, 2.0]])).all() + + +def test__traced_mesh_grid_pg_list(masked_imaging_7x7): + # Test Multi plane + + galaxy_no_pix = al.Galaxy( + redshift=0.5, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.5), + ) + + image_plane_mesh_grid_0 = al.Grid2D.no_mask( + values=[[[1.0, 0.0]]], pixel_scales=(1.0, 1.0) + ) + + galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) + + image_plane_mesh_grid_1 = al.Grid2D.no_mask( + values=[[[2.0, 0.0]]], pixel_scales=(1.0, 1.0) + ) + + galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) + + tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_pix_0, galaxy_pix_1]) + + adapt_images = al.AdaptImages( + galaxy_image_dict={galaxy_pix_0: 2, galaxy_pix_1: 3}, + galaxy_image_plane_mesh_grid_dict={ + galaxy_pix_0: image_plane_mesh_grid_0, + galaxy_pix_1: image_plane_mesh_grid_1, + }, + ) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images + ) + + traced_mesh_grids_list_of_planes = tracer_to_inversion.traced_mesh_grid_pg_list + + assert traced_mesh_grids_list_of_planes[0] == None + assert traced_mesh_grids_list_of_planes[1][0] == pytest.approx( + np.array([[1.0 - 0.5, 0.0]]), 1.0e-4 + ) + assert traced_mesh_grids_list_of_planes[1][1] == pytest.approx( + np.array([[2.0 - 0.5, 0.0]]), 1.0e-4 + ) + + # Test Extra Galaxies + + galaxy_pix_0 = al.Galaxy(redshift=1.0, pixelization=al.m.MockPixelization()) + galaxy_pix_1 = al.Galaxy(redshift=2.0, pixelization=al.m.MockPixelization()) + + adapt_images = al.AdaptImages( + galaxy_image_dict={galaxy_pix_0: 2, galaxy_pix_1: 3}, + galaxy_image_plane_mesh_grid_dict={ + galaxy_pix_0: image_plane_mesh_grid_0, + galaxy_pix_1: image_plane_mesh_grid_1, + }, + ) + + galaxy_no_pix_0 = al.Galaxy( + redshift=0.25, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.5), + ) + galaxy_no_pix_1 = al.Galaxy(redshift=0.5) + galaxy_no_pix_2 = al.Galaxy(redshift=1.5) + + tracer = al.Tracer( + galaxies=[ + galaxy_pix_0, + galaxy_pix_1, + galaxy_no_pix_0, + galaxy_no_pix_1, + galaxy_no_pix_2, + ] + ) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer, adapt_images=adapt_images + ) + + traced_mesh_grids_list_of_planes = tracer_to_inversion.traced_mesh_grid_pg_list + + traced_grid_pix_0 = tracer.traced_grid_2d_list_from( + grid=al.Grid2DIrregular(values=[[1.0, 0.0]]) + )[2] + traced_grid_pix_1 = tracer.traced_grid_2d_list_from( + grid=al.Grid2DIrregular(values=[[2.0, 0.0]]) + )[4] + + assert traced_mesh_grids_list_of_planes[0] == None + assert traced_mesh_grids_list_of_planes[1] == None + assert (traced_mesh_grids_list_of_planes[2][0] == traced_grid_pix_0).all() + assert traced_mesh_grids_list_of_planes[3] == None + assert (traced_mesh_grids_list_of_planes[4][0] == traced_grid_pix_1).all() + + +def test__mapper_galaxy_dict(masked_imaging_7x7): + galaxy_no_pix = al.Galaxy(redshift=0.5) + + tracer = al.Tracer(galaxies=[galaxy_no_pix, galaxy_no_pix]) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + mapper_galaxy_dict = tracer_to_inversion.mapper_galaxy_dict + assert mapper_galaxy_dict == {} + + galaxy_no_pix = al.Galaxy(redshift=0.5) + + pixelization_0 = al.m.MockPixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)) + ) + + galaxy_pix_0 = al.Galaxy(redshift=0.5, pixelization=pixelization_0) + + pixelization_1 = al.m.MockPixelization( + mesh=al.mesh.RectangularUniform(shape=(4, 3)) + ) + + galaxy_pix_1 = al.Galaxy(redshift=1.0, pixelization=pixelization_1) + + pixelization_2 = al.m.MockPixelization( + mesh=al.mesh.RectangularUniform(shape=(4, 4)) + ) + + galaxy_pix_2 = al.Galaxy(redshift=1.0, pixelization=pixelization_2) + + tracer = al.Tracer( + galaxies=[galaxy_no_pix, galaxy_pix_0, galaxy_pix_1, galaxy_pix_2] + ) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + mapper_galaxy_dict = tracer_to_inversion.mapper_galaxy_dict + + mapper_list = list(mapper_galaxy_dict.keys()) + + assert mapper_galaxy_dict[mapper_list[0]] == galaxy_pix_0 + assert mapper_galaxy_dict[mapper_list[1]] == galaxy_pix_1 + assert mapper_galaxy_dict[mapper_list[2]] == galaxy_pix_2 + + assert mapper_list[0].pixels == 9 + assert mapper_list[1].pixels == 12 + assert mapper_list[2].pixels == 16 + + galaxy_no_pix_0 = al.Galaxy( + redshift=0.25, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=0.5), + ) + galaxy_no_pix_1 = al.Galaxy(redshift=0.5) + galaxy_no_pix_2 = al.Galaxy(redshift=1.5) + + galaxy_pix_0 = al.Galaxy(redshift=0.75, pixelization=pixelization_0) + galaxy_pix_1 = al.Galaxy(redshift=2.0, pixelization=pixelization_1) + + tracer = al.Tracer( + galaxies=[ + galaxy_no_pix_0, + galaxy_no_pix_1, + galaxy_no_pix_2, + galaxy_pix_0, + galaxy_pix_1, + ] + ) + + tracer_to_inversion = al.TracerToInversion( + dataset=masked_imaging_7x7, tracer=tracer + ) + + mapper_galaxy_dict = tracer_to_inversion.mapper_galaxy_dict + + mapper_list = list(mapper_galaxy_dict.keys()) + + assert mapper_galaxy_dict[mapper_list[0]] == galaxy_pix_0 + assert mapper_galaxy_dict[mapper_list[1]] == galaxy_pix_1 + + assert mapper_galaxy_dict[mapper_list[0]] == galaxy_pix_0 + assert mapper_galaxy_dict[mapper_list[1]] == galaxy_pix_1 + + +def test__inversion_imaging_from(grid_2d_7x7, masked_imaging_7x7): + grids = al.GridsInterface( + lp=masked_imaging_7x7.grids.lp, + pixelization=masked_imaging_7x7.grids.pixelization, + blurring=masked_imaging_7x7.grids.blurring, + border_relocator=masked_imaging_7x7.grids.border_relocator, + ) + + dataset = al.DatasetInterface( + data=masked_imaging_7x7.data, + noise_map=masked_imaging_7x7.noise_map, + grids=grids, + psf=masked_imaging_7x7.psf, + ) + + g_linear = al.Galaxy( + redshift=0.5, light_linear=al.lp_linear.Sersic(centre=(0.05, 0.05)) + ) + + tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g_linear]) + + tracer_to_inversion = al.TracerToInversion( + dataset=dataset, + tracer=tracer, + ) + + inversion = tracer_to_inversion.inversion + + assert inversion.reconstruction[0] == pytest.approx(0.186868464426, 1.0e-2) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(3, 3)), + regularization=al.reg.Constant(coefficient=0.0), + ) + + g0 = al.Galaxy(redshift=0.5, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g0]) + + tracer_to_inversion = al.TracerToInversion( + dataset=dataset, + tracer=tracer, + ) + + inversion = tracer_to_inversion.inversion + + assert inversion.mapped_reconstructed_operated_data == pytest.approx( + masked_imaging_7x7.data, 1.0e-2 + ) + + +def test__inversion_interferometer_from(grid_2d_7x7, interferometer_7): + interferometer_7.data = al.Visibilities.ones(shape_slim=(7,)) + + grids = al.GridsInterface( + lp=interferometer_7.grids.lp, + pixelization=interferometer_7.grids.pixelization, + blurring=interferometer_7.grids.blurring, + border_relocator=interferometer_7.grids.border_relocator, + ) + + dataset = al.DatasetInterface( + data=interferometer_7.data, + noise_map=interferometer_7.noise_map, + grids=grids, + transformer=interferometer_7.transformer, + ) + + g_linear = al.Galaxy( + redshift=0.5, light_linear=al.lp_linear.Sersic(centre=(0.05, 0.05)) + ) + + tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g_linear]) + + tracer_to_inversion = al.TracerToInversion( + dataset=dataset, + tracer=tracer, + ) + + inversion = tracer_to_inversion.inversion + + assert inversion.reconstruction[0] == pytest.approx(0.0412484695, 1.0e-5) + + pixelization = al.Pixelization( + mesh=al.mesh.RectangularUniform(shape=(7, 7)), + regularization=al.reg.Constant(coefficient=0.0), + ) + + g0 = al.Galaxy(redshift=0.5, pixelization=pixelization) + + tracer = al.Tracer(galaxies=[al.Galaxy(redshift=0.5), g0]) + + tracer_to_inversion = al.TracerToInversion( + dataset=dataset, + tracer=tracer, + ) + + inversion = tracer_to_inversion.inversion + + assert inversion.reconstruction[0] == pytest.approx(-0.095834752881, 1.0e-4) diff --git a/test_autolens/lens/test_tracer_util.py b/test_autolens/lens/test_tracer_util.py index 285e3784a..e5292bef2 100644 --- a/test_autolens/lens/test_tracer_util.py +++ b/test_autolens/lens/test_tracer_util.py @@ -1,272 +1,272 @@ -import numpy as np -import pytest - -import autolens as al - - -def test__traced_grid_2d_list_from(grid_2d_7x7_simple): - g0 = al.Galaxy(redshift=2.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) - g1 = al.Galaxy(redshift=2.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) - g2 = al.Galaxy(redshift=0.1, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) - g3 = al.Galaxy(redshift=3.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) - g4 = al.Galaxy(redshift=1.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) - g5 = al.Galaxy(redshift=3.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) - - galaxies = [g0, g1, g2, g3, g4, g5] - - planes = al.util.tracer.planes_from(galaxies=galaxies) - - traced_grid_list = al.util.tracer.traced_grid_2d_list_from( - planes=planes, grid=grid_2d_7x7_simple, cosmology=al.cosmo.Planck15() - ) - - # The scaling factors are as follows and were computed independently from the test_autoarray. - beta_01 = 0.9348 - beta_02 = 0.9839601 - beta_12 = 0.7539734 - - val = np.sqrt(2) / 2.0 - - assert traced_grid_list[0][0] == pytest.approx(np.array([1.0, 1.0]), 1e-4) - assert traced_grid_list[0][1] == pytest.approx(np.array([1.0, 0.0]), 1e-4) - - assert traced_grid_list[1][0] == pytest.approx( - np.array([(1.0 - beta_01 * val), (1.0 - beta_01 * val)]), 1e-4 - ) - assert traced_grid_list[1][1] == pytest.approx( - np.array([(1.0 - beta_01 * 1.0), 0.0]), 1e-4 - ) - - defl11 = g0.deflections_yx_2d_from( - grid=al.Grid2DIrregular([[(1.0 - beta_01 * val), (1.0 - beta_01 * val)]]) - ) - defl12 = g0.deflections_yx_2d_from( - grid=al.Grid2DIrregular([[(1.0 - beta_01 * 1.0), 0.0]]) - ) - - assert traced_grid_list[2][0] == pytest.approx( - np.array( - [ - (1.0 - beta_02 * val - beta_12 * defl11.array[0, 0]), - (1.0 - beta_02 * val - beta_12 * defl11.array[0, 1]), - ] - ), - 1e-4, - ) - assert traced_grid_list[2][1] == pytest.approx( - np.array([(1.0 - beta_02 * 1.0 - beta_12 * defl12.array[0, 0]), 0.0]), 1e-4 - ) - - assert traced_grid_list[3][1] == pytest.approx(np.array([1.0, 0.0]), 1e-4) - - traced_grid_list = al.util.tracer.traced_grid_2d_list_from( - planes=planes, - grid=grid_2d_7x7_simple, - plane_index_limit=1, - cosmology=al.cosmo.Planck15(), - ) - - # The scaling factors are as follows and were computed independently from the test_autoarray. - beta_01 = 0.9348 - - val = np.sqrt(2) / 2.0 - - assert traced_grid_list[0][0] == pytest.approx(np.array([1.0, 1.0]), 1e-4) - assert traced_grid_list[0][1] == pytest.approx(np.array([1.0, 0.0]), 1e-4) - - assert traced_grid_list[1][0] == pytest.approx( - np.array([(1.0 - beta_01 * val), (1.0 - beta_01 * val)]), 1e-4 - ) - assert traced_grid_list[1][1] == pytest.approx( - np.array([(1.0 - beta_01 * 1.0), 0.0]), 1e-4 - ) - - assert len(traced_grid_list) == 2 - - -def test__grid_2d_at_redshift_from(grid_2d_7x7): - g0 = al.Galaxy( - redshift=0.5, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ) - g1 = al.Galaxy( - redshift=0.75, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=2.0), - ) - g2 = al.Galaxy( - redshift=1.5, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=3.0), - ) - g3 = al.Galaxy( - redshift=1.0, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=4.0), - ) - g4 = al.Galaxy(redshift=2.0) - - galaxies = [g0, g1, g2, g3, g4] - planes = al.util.tracer.planes_from(galaxies=galaxies) - - traced_grid_list = al.util.tracer.traced_grid_2d_list_from( - planes=planes, grid=grid_2d_7x7 - ) - - grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( - galaxies=galaxies, grid=grid_2d_7x7, redshift=0.5 - ) - - assert grid_at_redshift == pytest.approx(traced_grid_list[0], 1.0e-4) - - grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( - galaxies=galaxies, grid=grid_2d_7x7, redshift=0.75 - ) - - assert grid_at_redshift == pytest.approx(traced_grid_list[1].array, 1.0e-4) - - grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( - galaxies=galaxies, grid=grid_2d_7x7, redshift=1.0 - ) - - assert grid_at_redshift == pytest.approx(traced_grid_list[2].array, 1.0e-4) - - grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( - galaxies=galaxies, grid=grid_2d_7x7, redshift=1.5 - ) - - assert grid_at_redshift == pytest.approx(traced_grid_list[3].array, 1.0e-4) - - grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( - galaxies=galaxies, grid=grid_2d_7x7, redshift=2.0 - ) - - assert grid_at_redshift == pytest.approx(traced_grid_list[4].array, 1.0e-4) - - -def test__grid_2d_at_redshift_from__redshift_between_planes(grid_2d_7x7): - grid_2d_7x7[0] = al.Grid2DIrregular([[1.0, -1.0]]) - grid_2d_7x7[1] = al.Grid2DIrregular([[1.0, 0.0]]) - - g0 = al.Galaxy( - redshift=0.5, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), - ) - g1 = al.Galaxy( - redshift=0.75, - mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=2.0), - ) - g2 = al.Galaxy(redshift=2.0) - - galaxies = [g0, g1, g2] - - grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( - galaxies=galaxies, grid=grid_2d_7x7, redshift=1.9 - ) - - assert grid_at_redshift[0][0] == pytest.approx(-1.06587, 1.0e-1) - assert grid_at_redshift[0][1] == pytest.approx(1.06587, 1.0e-1) - assert grid_at_redshift[1][0] == pytest.approx(-1.921583, 1.0e-1) - assert grid_at_redshift[1][1] == pytest.approx(0.0, 1.0e-1) - - grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( - galaxies=galaxies, - grid=grid_2d_7x7.mask.derive_grid.all_false, - redshift=0.3, - ) - - assert (grid_at_redshift == grid_2d_7x7.mask.derive_grid.all_false).all() - - -class _FakeTracedRedshift: - """A redshift-like object that mimics a JAX traced scalar — calling ``float()`` - raises, so ``tracer_util._redshift_is_traced`` should return True. Used to - exercise the JAX partition-and-splice path without importing ``jax`` (library - unit tests stay numpy-only — see ``feedback_no_jax_in_unit_tests``).""" - - def __init__(self, name: str): - self.name = name - - def __float__(self): - raise TypeError("traced redshift cannot be coerced to float") - - def __repr__(self): - return f"" - - -def test__redshift_is_traced__detects_traced_and_concrete(): - from autolens.lens import tracer_util - - assert tracer_util._redshift_is_traced(_FakeTracedRedshift("subhalo")) is True - - assert tracer_util._redshift_is_traced(0.5) is False - assert tracer_util._redshift_is_traced(1) is False - assert tracer_util._redshift_is_traced(np.float64(1.5)) is False - assert tracer_util._redshift_is_traced(np.array(0.7)) is False - - -def test__plane_redshifts_from__partition_path__preserves_input_order(): - from autolens.lens import tracer_util - - lens = al.Galaxy(redshift=0.5) - subhalo = al.Galaxy(redshift=_FakeTracedRedshift("subhalo")) - source = al.Galaxy(redshift=1.0) - - plane_redshifts = tracer_util.plane_redshifts_from( - galaxies=[lens, subhalo, source] - ) - - assert plane_redshifts[0] == 0.5 - assert isinstance(plane_redshifts[1], _FakeTracedRedshift) - assert plane_redshifts[2] == 1.0 - - -def test__plane_redshifts_from__partition_path__dedupes_concrete_only(): - from autolens.lens import tracer_util - - g0 = al.Galaxy(redshift=0.5) - g1 = al.Galaxy(redshift=0.5) # duplicate concrete redshift — should collapse - subhalo = al.Galaxy(redshift=_FakeTracedRedshift("subhalo")) - source = al.Galaxy(redshift=1.0) - - plane_redshifts = tracer_util.plane_redshifts_from( - galaxies=[g0, g1, subhalo, source] - ) - - assert plane_redshifts[0] == 0.5 - assert isinstance(plane_redshifts[1], _FakeTracedRedshift) - assert plane_redshifts[2] == 1.0 - assert len(plane_redshifts) == 3 - - -def test__planes_from__partition_path__traced_galaxy_gets_dedicated_plane(): - from autolens.lens import tracer_util - - lens_a = al.Galaxy(redshift=0.5) - lens_b = al.Galaxy(redshift=0.5) # same plane as lens_a - subhalo = al.Galaxy(redshift=_FakeTracedRedshift("subhalo")) - source = al.Galaxy(redshift=1.0) - - planes = tracer_util.planes_from(galaxies=[lens_a, lens_b, subhalo, source]) - - assert len(planes) == 3 - assert list(planes[0]) == [lens_a, lens_b] - assert list(planes[1]) == [subhalo] - assert list(planes[2]) == [source] - - -def test__time_delays_from(): - - grid = al.Grid2DIrregular(values=[(0.7, 0.5), (1.0, 1.0)]) - - mp = al.mp.Isothermal( - centre=(0.0, 0.0), ell_comps=(0.0, -0.111111), einstein_radius=2.0 - ) - - lens = al.Galaxy(redshift=0.2, mass=mp) - source = al.Galaxy(redshift=0.7) - - time_delay = al.util.tracer.time_delays_from( - galaxies=al.Galaxies([lens, source]), - grid=grid, - cosmology=al.cosmo.Planck15(), - ) - - assert time_delay == pytest.approx(np.array([8.52966247, -29.0176387]), 1.0e-4) +import numpy as np +import pytest + +import autolens as al + + +def test__traced_grid_2d_list_from(grid_2d_7x7_simple): + g0 = al.Galaxy(redshift=2.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) + g1 = al.Galaxy(redshift=2.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) + g2 = al.Galaxy(redshift=0.1, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) + g3 = al.Galaxy(redshift=3.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) + g4 = al.Galaxy(redshift=1.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) + g5 = al.Galaxy(redshift=3.0, mass_profile=al.mp.IsothermalSph(einstein_radius=1.0)) + + galaxies = [g0, g1, g2, g3, g4, g5] + + planes = al.util.tracer.planes_from(galaxies=galaxies) + + traced_grid_list = al.util.tracer.traced_grid_2d_list_from( + planes=planes, grid=grid_2d_7x7_simple, cosmology=al.cosmo.Planck15() + ) + + # The scaling factors are as follows and were computed independently from the test_autoarray. + beta_01 = 0.9348 + beta_02 = 0.9839601 + beta_12 = 0.7539734 + + val = np.sqrt(2) / 2.0 + + assert traced_grid_list[0][0] == pytest.approx(np.array([1.0, 1.0]), 1e-4) + assert traced_grid_list[0][1] == pytest.approx(np.array([1.0, 0.0]), 1e-4) + + assert traced_grid_list[1][0] == pytest.approx( + np.array([(1.0 - beta_01 * val), (1.0 - beta_01 * val)]), 1e-4 + ) + assert traced_grid_list[1][1] == pytest.approx( + np.array([(1.0 - beta_01 * 1.0), 0.0]), 1e-4 + ) + + defl11 = g0.deflections_yx_2d_from( + grid=al.Grid2DIrregular([[(1.0 - beta_01 * val), (1.0 - beta_01 * val)]]) + ) + defl12 = g0.deflections_yx_2d_from( + grid=al.Grid2DIrregular([[(1.0 - beta_01 * 1.0), 0.0]]) + ) + + assert traced_grid_list[2][0] == pytest.approx( + np.array( + [ + (1.0 - beta_02 * val - beta_12 * defl11.array[0, 0]), + (1.0 - beta_02 * val - beta_12 * defl11.array[0, 1]), + ] + ), + 1e-4, + ) + assert traced_grid_list[2][1] == pytest.approx( + np.array([(1.0 - beta_02 * 1.0 - beta_12 * defl12.array[0, 0]), 0.0]), 1e-4 + ) + + assert traced_grid_list[3][1] == pytest.approx(np.array([1.0, 0.0]), 1e-4) + + traced_grid_list = al.util.tracer.traced_grid_2d_list_from( + planes=planes, + grid=grid_2d_7x7_simple, + plane_index_limit=1, + cosmology=al.cosmo.Planck15(), + ) + + # The scaling factors are as follows and were computed independently from the test_autoarray. + beta_01 = 0.9348 + + val = np.sqrt(2) / 2.0 + + assert traced_grid_list[0][0] == pytest.approx(np.array([1.0, 1.0]), 1e-4) + assert traced_grid_list[0][1] == pytest.approx(np.array([1.0, 0.0]), 1e-4) + + assert traced_grid_list[1][0] == pytest.approx( + np.array([(1.0 - beta_01 * val), (1.0 - beta_01 * val)]), 1e-4 + ) + assert traced_grid_list[1][1] == pytest.approx( + np.array([(1.0 - beta_01 * 1.0), 0.0]), 1e-4 + ) + + assert len(traced_grid_list) == 2 + + +def test__grid_2d_at_redshift_from(grid_2d_7x7): + g0 = al.Galaxy( + redshift=0.5, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ) + g1 = al.Galaxy( + redshift=0.75, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=2.0), + ) + g2 = al.Galaxy( + redshift=1.5, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=3.0), + ) + g3 = al.Galaxy( + redshift=1.0, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=4.0), + ) + g4 = al.Galaxy(redshift=2.0) + + galaxies = [g0, g1, g2, g3, g4] + planes = al.util.tracer.planes_from(galaxies=galaxies) + + traced_grid_list = al.util.tracer.traced_grid_2d_list_from( + planes=planes, grid=grid_2d_7x7 + ) + + grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( + galaxies=galaxies, grid=grid_2d_7x7, redshift=0.5 + ) + + assert grid_at_redshift == pytest.approx(traced_grid_list[0], 1.0e-4) + + grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( + galaxies=galaxies, grid=grid_2d_7x7, redshift=0.75 + ) + + assert grid_at_redshift == pytest.approx(traced_grid_list[1].array, 1.0e-4) + + grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( + galaxies=galaxies, grid=grid_2d_7x7, redshift=1.0 + ) + + assert grid_at_redshift == pytest.approx(traced_grid_list[2].array, 1.0e-4) + + grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( + galaxies=galaxies, grid=grid_2d_7x7, redshift=1.5 + ) + + assert grid_at_redshift == pytest.approx(traced_grid_list[3].array, 1.0e-4) + + grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( + galaxies=galaxies, grid=grid_2d_7x7, redshift=2.0 + ) + + assert grid_at_redshift == pytest.approx(traced_grid_list[4].array, 1.0e-4) + + +def test__grid_2d_at_redshift_from__redshift_between_planes(grid_2d_7x7): + grid_2d_7x7[0] = al.Grid2DIrregular([[1.0, -1.0]]) + grid_2d_7x7[1] = al.Grid2DIrregular([[1.0, 0.0]]) + + g0 = al.Galaxy( + redshift=0.5, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=1.0), + ) + g1 = al.Galaxy( + redshift=0.75, + mass_profile=al.mp.IsothermalSph(centre=(0.0, 0.0), einstein_radius=2.0), + ) + g2 = al.Galaxy(redshift=2.0) + + galaxies = [g0, g1, g2] + + grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( + galaxies=galaxies, grid=grid_2d_7x7, redshift=1.9 + ) + + assert grid_at_redshift[0][0] == pytest.approx(-1.06587, 1.0e-1) + assert grid_at_redshift[0][1] == pytest.approx(1.06587, 1.0e-1) + assert grid_at_redshift[1][0] == pytest.approx(-1.921583, 1.0e-1) + assert grid_at_redshift[1][1] == pytest.approx(0.0, 1.0e-1) + + grid_at_redshift = al.util.tracer.grid_2d_at_redshift_from( + galaxies=galaxies, + grid=grid_2d_7x7.mask.derive_grid.all_false, + redshift=0.3, + ) + + assert (grid_at_redshift == grid_2d_7x7.mask.derive_grid.all_false).all() + + +class _FakeTracedRedshift: + """A redshift-like object that mimics a JAX traced scalar — calling ``float()`` + raises, so ``tracer_util._redshift_is_traced`` should return True. Used to + exercise the JAX partition-and-splice path without importing ``jax`` (library + unit tests stay numpy-only — see ``feedback_no_jax_in_unit_tests``).""" + + def __init__(self, name: str): + self.name = name + + def __float__(self): + raise TypeError("traced redshift cannot be coerced to float") + + def __repr__(self): + return f"" + + +def test__redshift_is_traced__detects_traced_and_concrete(): + from autolens.lens import tracer_util + + assert tracer_util._redshift_is_traced(_FakeTracedRedshift("subhalo")) is True + + assert tracer_util._redshift_is_traced(0.5) is False + assert tracer_util._redshift_is_traced(1) is False + assert tracer_util._redshift_is_traced(np.float64(1.5)) is False + assert tracer_util._redshift_is_traced(np.array(0.7)) is False + + +def test__plane_redshifts_from__partition_path__preserves_input_order(): + from autolens.lens import tracer_util + + lens = al.Galaxy(redshift=0.5) + subhalo = al.Galaxy(redshift=_FakeTracedRedshift("subhalo")) + source = al.Galaxy(redshift=1.0) + + plane_redshifts = tracer_util.plane_redshifts_from( + galaxies=[lens, subhalo, source] + ) + + assert plane_redshifts[0] == 0.5 + assert isinstance(plane_redshifts[1], _FakeTracedRedshift) + assert plane_redshifts[2] == 1.0 + + +def test__plane_redshifts_from__partition_path__dedupes_concrete_only(): + from autolens.lens import tracer_util + + g0 = al.Galaxy(redshift=0.5) + g1 = al.Galaxy(redshift=0.5) # duplicate concrete redshift — should collapse + subhalo = al.Galaxy(redshift=_FakeTracedRedshift("subhalo")) + source = al.Galaxy(redshift=1.0) + + plane_redshifts = tracer_util.plane_redshifts_from( + galaxies=[g0, g1, subhalo, source] + ) + + assert plane_redshifts[0] == 0.5 + assert isinstance(plane_redshifts[1], _FakeTracedRedshift) + assert plane_redshifts[2] == 1.0 + assert len(plane_redshifts) == 3 + + +def test__planes_from__partition_path__traced_galaxy_gets_dedicated_plane(): + from autolens.lens import tracer_util + + lens_a = al.Galaxy(redshift=0.5) + lens_b = al.Galaxy(redshift=0.5) # same plane as lens_a + subhalo = al.Galaxy(redshift=_FakeTracedRedshift("subhalo")) + source = al.Galaxy(redshift=1.0) + + planes = tracer_util.planes_from(galaxies=[lens_a, lens_b, subhalo, source]) + + assert len(planes) == 3 + assert list(planes[0]) == [lens_a, lens_b] + assert list(planes[1]) == [subhalo] + assert list(planes[2]) == [source] + + +def test__time_delays_from(): + + grid = al.Grid2DIrregular(values=[(0.7, 0.5), (1.0, 1.0)]) + + mp = al.mp.Isothermal( + centre=(0.0, 0.0), ell_comps=(0.0, -0.111111), einstein_radius=2.0 + ) + + lens = al.Galaxy(redshift=0.2, mass=mp) + source = al.Galaxy(redshift=0.7) + + time_delay = al.util.tracer.time_delays_from( + galaxies=al.Galaxies([lens, source]), + grid=grid, + cosmology=al.cosmo.Planck15(), + ) + + assert time_delay == pytest.approx(np.array([8.52966247, -29.0176387]), 1.0e-4) diff --git a/test_autolens/point/files/point_dict.json b/test_autolens/point/files/point_dict.json index 6f3bd0e8e..ab232721d 100644 --- a/test_autolens/point/files/point_dict.json +++ b/test_autolens/point/files/point_dict.json @@ -1,36 +1,36 @@ -[ - { - "name": "source_1", - "positions": [ - [ - 1.0, - 1.0 - ] - ], - "positions_noise_map": [ - 1.0 - ], - "fluxes": null, - "fluxes_noise_map": null - }, - { - "name": "source_2", - "positions": [ - [ - 1.0, - 1.0 - ] - ], - "positions_noise_map": [ - 1.0 - ], - "fluxes": [ - 2.0, - 3.0 - ], - "fluxes_noise_map": [ - 4.0, - 5.0 - ] - } +[ + { + "name": "source_1", + "positions": [ + [ + 1.0, + 1.0 + ] + ], + "positions_noise_map": [ + 1.0 + ], + "fluxes": null, + "fluxes_noise_map": null + }, + { + "name": "source_2", + "positions": [ + [ + 1.0, + 1.0 + ] + ], + "positions_noise_map": [ + 1.0 + ], + "fluxes": [ + 2.0, + 3.0 + ], + "fluxes_noise_map": [ + 4.0, + 5.0 + ] + } ] \ No newline at end of file diff --git a/test_autolens/point/fit/test_abstract.py b/test_autolens/point/fit/test_abstract.py index ddceeb56d..901046bc1 100644 --- a/test_autolens/point/fit/test_abstract.py +++ b/test_autolens/point/fit/test_abstract.py @@ -1,46 +1,46 @@ -import pytest - -import autogalaxy as ag -import autolens as al - - -def test__magnifications_at_positions__multi_plane_calculation(gal_x1_mp): - g0 = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph(einstein_radius=1.0)) - g1 = al.Galaxy(redshift=1.0, point_0=al.ps.PointFlux(flux=1.0)) - g2 = al.Galaxy(redshift=2.0, point_1=al.ps.PointFlux(flux=2.0)) - - tracer = al.Tracer(galaxies=[g0, g1, g2]) - - data = al.ArrayIrregular([1.0]) - noise_map = al.ArrayIrregular([3.0]) - positions = al.Grid2DIrregular([(2.0, 0.0)]) - - fit_0 = al.FitFluxes( - name="point_0", - data=data, - noise_map=noise_map, - positions=positions, - tracer=tracer, - ) - - od_0 = ag.LensCalc.from_tracer(tracer, use_multi_plane=True, plane_j=1) - magnification_0 = abs(od_0.magnification_2d_via_hessian_from(grid=positions)) - - assert fit_0.magnifications_at_positions[0] == magnification_0 - - fit_1 = al.FitFluxes( - name="point_1", - data=data, - noise_map=noise_map, - positions=positions, - tracer=tracer, - ) - - od_1 = ag.LensCalc.from_tracer(tracer, use_multi_plane=True, plane_j=2) - magnification_1 = abs(od_1.magnification_2d_via_hessian_from(grid=positions)) - - assert fit_1.magnifications_at_positions[0] == magnification_1 - - assert fit_0.magnifications_at_positions[0] != pytest.approx( - fit_1.magnifications_at_positions.array[0], 1.0e-1 - ) +import pytest + +import autogalaxy as ag +import autolens as al + + +def test__magnifications_at_positions__multi_plane_calculation(gal_x1_mp): + g0 = al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph(einstein_radius=1.0)) + g1 = al.Galaxy(redshift=1.0, point_0=al.ps.PointFlux(flux=1.0)) + g2 = al.Galaxy(redshift=2.0, point_1=al.ps.PointFlux(flux=2.0)) + + tracer = al.Tracer(galaxies=[g0, g1, g2]) + + data = al.ArrayIrregular([1.0]) + noise_map = al.ArrayIrregular([3.0]) + positions = al.Grid2DIrregular([(2.0, 0.0)]) + + fit_0 = al.FitFluxes( + name="point_0", + data=data, + noise_map=noise_map, + positions=positions, + tracer=tracer, + ) + + od_0 = ag.LensCalc.from_tracer(tracer, use_multi_plane=True, plane_j=1) + magnification_0 = abs(od_0.magnification_2d_via_hessian_from(grid=positions)) + + assert fit_0.magnifications_at_positions[0] == magnification_0 + + fit_1 = al.FitFluxes( + name="point_1", + data=data, + noise_map=noise_map, + positions=positions, + tracer=tracer, + ) + + od_1 = ag.LensCalc.from_tracer(tracer, use_multi_plane=True, plane_j=2) + magnification_1 = abs(od_1.magnification_2d_via_hessian_from(grid=positions)) + + assert fit_1.magnifications_at_positions[0] == magnification_1 + + assert fit_0.magnifications_at_positions[0] != pytest.approx( + fit_1.magnifications_at_positions.array[0], 1.0e-1 + ) diff --git a/test_autolens/point/model/test_analysis_point.py b/test_autolens/point/model/test_analysis_point.py index 255246f9a..218009db3 100644 --- a/test_autolens/point/model/test_analysis_point.py +++ b/test_autolens/point/model/test_analysis_point.py @@ -1,208 +1,208 @@ -from pathlib import Path -import importlib.util - -import pytest - -import autofit as af -import autolens as al - -from autolens.point.model.result import ResultPoint - -directory = Path(__file__).resolve().parent - - -def _jax_installed() -> bool: - return importlib.util.find_spec("jax") is not None - - -def test__pyauto_disable_jax_env_downgrades_use_jax__point( - monkeypatch, point_dataset -): - # THE BUG TEST. Before the one-reader fix `AnalysisPoint` had no local - # env read and `AnalysisLens.__init__` overwrote the base-resolved - # `self._use_jax` with the raw `use_jax` parameter, so the disable-jax - # env var was silently a no-op (base set False, AnalysisLens set True). - # It must now downgrade to False. - monkeypatch.setenv("PYAUTO_DISABLE_JAX", "1") - - solver = al.m.MockPointSolver(model_positions=point_dataset.positions) - - analysis = al.AnalysisPoint( - dataset=point_dataset, solver=solver, use_jax=True - ) - - assert analysis._use_jax is False - - -@pytest.mark.skipif(not _jax_installed(), reason="jax not installed") -def test__use_jax_true_env_unset__not_downgraded__point( - monkeypatch, point_dataset -): - # No over-downgrade: with the env var unset and jax installed, - # `use_jax=True` must survive as `self._use_jax is True`. - monkeypatch.delenv("PYAUTO_DISABLE_JAX", raising=False) - - solver = al.m.MockPointSolver(model_positions=point_dataset.positions) - - analysis = al.AnalysisPoint( - dataset=point_dataset, solver=solver, use_jax=True - ) - - assert analysis._use_jax is True - - -def _test__make_result__result_imaging_is_returned(point_dataset): - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy(redshift=0.5, point_0=al.ps.Point(centre=(0.0, 0.0))) - ) - ) - - search = al.m.MockSearch(name="test_search") - - solver = al.m.MockPointSolver(model_positions=point_dataset.positions) - - analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) - - result = search.fit(model=model, analysis=analysis) - - assert isinstance(result, ResultPoint) - - -def test__figure_of_merit__matches_correct_fit_given_galaxy_profiles( - positions_x2, positions_x2_noise_map -): - point_dataset = al.PointDataset( - name="point_0", - positions=positions_x2, - positions_noise_map=positions_x2_noise_map, - ) - - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy(redshift=0.5, point_0=al.ps.Point(centre=(0.0, 0.0))) - ) - ) - - solver = al.m.MockPointSolver(model_positions=positions_x2) - - analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) - - instance = model.instance_from_unit_vector([]) - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - tracer = analysis.tracer_via_instance_from(instance=instance) - - fit_positions = al.FitPositionsImagePairRepeat( - name="point_0", - data=positions_x2, - noise_map=positions_x2_noise_map, - tracer=tracer, - solver=solver, - ) - - assert fit_positions.chi_squared == 0.0 - assert fit_positions.log_likelihood == analysis_log_likelihood - - model_positions = al.Grid2DIrregular([(0.0, 1.0), (1.0, 2.0)]) - solver = al.m.MockPointSolver(model_positions=model_positions) - - analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) - - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - fit_positions = al.FitPositionsImagePairRepeat( - name="point_0", - data=positions_x2, - noise_map=positions_x2_noise_map, - tracer=tracer, - solver=solver, - ) - - assert fit_positions.residual_map.in_list == [1.0, 1.0] - assert fit_positions.chi_squared == 2.0 - assert fit_positions.log_likelihood == analysis_log_likelihood - - -def test__figure_of_merit__includes_fit_fluxes( - positions_x2, positions_x2_noise_map, fluxes_x2, fluxes_x2_noise_map -): - point_dataset = al.PointDataset( - name="point_0", - positions=positions_x2, - positions_noise_map=positions_x2_noise_map, - fluxes=fluxes_x2, - fluxes_noise_map=fluxes_x2_noise_map, - ) - - model = af.Collection( - galaxies=af.Collection( - lens=al.Galaxy( - redshift=0.5, - sis=al.mp.IsothermalSph(einstein_radius=1.0), - point_0=al.ps.PointFlux(flux=1.0), - ) - ) - ) - - solver = al.m.MockPointSolver(model_positions=positions_x2) - - analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) - - instance = model.instance_from_unit_vector([]) - - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - tracer = analysis.tracer_via_instance_from(instance=instance) - - fit_positions = al.FitPositionsImagePairRepeat( - name="point_0", - data=positions_x2, - noise_map=positions_x2_noise_map, - tracer=tracer, - solver=solver, - ) - - fit_fluxes = al.FitFluxes( - name="point_0", - data=fluxes_x2, - noise_map=fluxes_x2_noise_map, - positions=positions_x2, - tracer=tracer, - ) - - assert ( - fit_positions.log_likelihood + fit_fluxes.log_likelihood - == analysis_log_likelihood - ) - - model_positions = al.Grid2DIrregular([(0.0, 1.0), (1.0, 2.0)]) - solver = al.m.MockPointSolver(model_positions=model_positions) - - analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) - - instance = model.instance_from_unit_vector([]) - analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) - - fit_positions = al.FitPositionsImagePairRepeat( - name="point_0", - data=positions_x2, - noise_map=positions_x2_noise_map, - tracer=tracer, - solver=solver, - ) - - fit_fluxes = al.FitFluxes( - name="point_0", - data=fluxes_x2, - noise_map=fluxes_x2_noise_map, - positions=positions_x2, - tracer=tracer, - ) - - assert fit_positions.residual_map.in_list == [1.0, 1.0] - assert fit_positions.chi_squared == 2.0 - assert ( - fit_positions.log_likelihood + fit_fluxes.log_likelihood - == analysis_log_likelihood - ) +from pathlib import Path +import importlib.util + +import pytest + +import autofit as af +import autolens as al + +from autolens.point.model.result import ResultPoint + +directory = Path(__file__).resolve().parent + + +def _jax_installed() -> bool: + return importlib.util.find_spec("jax") is not None + + +def test__pyauto_disable_jax_env_downgrades_use_jax__point( + monkeypatch, point_dataset +): + # THE BUG TEST. Before the one-reader fix `AnalysisPoint` had no local + # env read and `AnalysisLens.__init__` overwrote the base-resolved + # `self._use_jax` with the raw `use_jax` parameter, so the disable-jax + # env var was silently a no-op (base set False, AnalysisLens set True). + # It must now downgrade to False. + monkeypatch.setenv("PYAUTO_DISABLE_JAX", "1") + + solver = al.m.MockPointSolver(model_positions=point_dataset.positions) + + analysis = al.AnalysisPoint( + dataset=point_dataset, solver=solver, use_jax=True + ) + + assert analysis._use_jax is False + + +@pytest.mark.skipif(not _jax_installed(), reason="jax not installed") +def test__use_jax_true_env_unset__not_downgraded__point( + monkeypatch, point_dataset +): + # No over-downgrade: with the env var unset and jax installed, + # `use_jax=True` must survive as `self._use_jax is True`. + monkeypatch.delenv("PYAUTO_DISABLE_JAX", raising=False) + + solver = al.m.MockPointSolver(model_positions=point_dataset.positions) + + analysis = al.AnalysisPoint( + dataset=point_dataset, solver=solver, use_jax=True + ) + + assert analysis._use_jax is True + + +def _test__make_result__result_imaging_is_returned(point_dataset): + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy(redshift=0.5, point_0=al.ps.Point(centre=(0.0, 0.0))) + ) + ) + + search = al.m.MockSearch(name="test_search") + + solver = al.m.MockPointSolver(model_positions=point_dataset.positions) + + analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) + + result = search.fit(model=model, analysis=analysis) + + assert isinstance(result, ResultPoint) + + +def test__figure_of_merit__matches_correct_fit_given_galaxy_profiles( + positions_x2, positions_x2_noise_map +): + point_dataset = al.PointDataset( + name="point_0", + positions=positions_x2, + positions_noise_map=positions_x2_noise_map, + ) + + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy(redshift=0.5, point_0=al.ps.Point(centre=(0.0, 0.0))) + ) + ) + + solver = al.m.MockPointSolver(model_positions=positions_x2) + + analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) + + instance = model.instance_from_unit_vector([]) + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + tracer = analysis.tracer_via_instance_from(instance=instance) + + fit_positions = al.FitPositionsImagePairRepeat( + name="point_0", + data=positions_x2, + noise_map=positions_x2_noise_map, + tracer=tracer, + solver=solver, + ) + + assert fit_positions.chi_squared == 0.0 + assert fit_positions.log_likelihood == analysis_log_likelihood + + model_positions = al.Grid2DIrregular([(0.0, 1.0), (1.0, 2.0)]) + solver = al.m.MockPointSolver(model_positions=model_positions) + + analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) + + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + fit_positions = al.FitPositionsImagePairRepeat( + name="point_0", + data=positions_x2, + noise_map=positions_x2_noise_map, + tracer=tracer, + solver=solver, + ) + + assert fit_positions.residual_map.in_list == [1.0, 1.0] + assert fit_positions.chi_squared == 2.0 + assert fit_positions.log_likelihood == analysis_log_likelihood + + +def test__figure_of_merit__includes_fit_fluxes( + positions_x2, positions_x2_noise_map, fluxes_x2, fluxes_x2_noise_map +): + point_dataset = al.PointDataset( + name="point_0", + positions=positions_x2, + positions_noise_map=positions_x2_noise_map, + fluxes=fluxes_x2, + fluxes_noise_map=fluxes_x2_noise_map, + ) + + model = af.Collection( + galaxies=af.Collection( + lens=al.Galaxy( + redshift=0.5, + sis=al.mp.IsothermalSph(einstein_radius=1.0), + point_0=al.ps.PointFlux(flux=1.0), + ) + ) + ) + + solver = al.m.MockPointSolver(model_positions=positions_x2) + + analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) + + instance = model.instance_from_unit_vector([]) + + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + tracer = analysis.tracer_via_instance_from(instance=instance) + + fit_positions = al.FitPositionsImagePairRepeat( + name="point_0", + data=positions_x2, + noise_map=positions_x2_noise_map, + tracer=tracer, + solver=solver, + ) + + fit_fluxes = al.FitFluxes( + name="point_0", + data=fluxes_x2, + noise_map=fluxes_x2_noise_map, + positions=positions_x2, + tracer=tracer, + ) + + assert ( + fit_positions.log_likelihood + fit_fluxes.log_likelihood + == analysis_log_likelihood + ) + + model_positions = al.Grid2DIrregular([(0.0, 1.0), (1.0, 2.0)]) + solver = al.m.MockPointSolver(model_positions=model_positions) + + analysis = al.AnalysisPoint(dataset=point_dataset, solver=solver, use_jax=False) + + instance = model.instance_from_unit_vector([]) + analysis_log_likelihood = analysis.log_likelihood_function(instance=instance) + + fit_positions = al.FitPositionsImagePairRepeat( + name="point_0", + data=positions_x2, + noise_map=positions_x2_noise_map, + tracer=tracer, + solver=solver, + ) + + fit_fluxes = al.FitFluxes( + name="point_0", + data=fluxes_x2, + noise_map=fluxes_x2_noise_map, + positions=positions_x2, + tracer=tracer, + ) + + assert fit_positions.residual_map.in_list == [1.0, 1.0] + assert fit_positions.chi_squared == 2.0 + assert ( + fit_positions.log_likelihood + fit_fluxes.log_likelihood + == analysis_log_likelihood + ) diff --git a/test_autolens/point/test_point_source_dataset.py b/test_autolens/point/test_point_source_dataset.py index f423477c7..3b31fb79f 100644 --- a/test_autolens/point/test_point_source_dataset.py +++ b/test_autolens/point/test_point_source_dataset.py @@ -1,21 +1,21 @@ -import autolens as al - - -def test__info(): - dataset = al.PointDataset( - "name", - positions=al.Grid2DIrregular([(1, 2)]), - positions_noise_map=al.ArrayIrregular([1]), - fluxes=al.ArrayIrregular([2]), - fluxes_noise_map=al.ArrayIrregular([3]), - time_delays=al.ArrayIrregular([4]), - time_delays_noise_map=al.ArrayIrregular([5]), - ) - - assert "name" in dataset.info - assert "positions : Grid2DIrregular" in dataset.info - assert "positions_noise_map : ArrayIrregular" in dataset.info - assert "fluxes : ArrayIrregular" in dataset.info - assert "fluxes_noise_map : ArrayIrregular" in dataset.info - assert "time_delays : ArrayIrregular" in dataset.info - assert "time_delays_noise_map : ArrayIrregular" in dataset.info +import autolens as al + + +def test__info(): + dataset = al.PointDataset( + "name", + positions=al.Grid2DIrregular([(1, 2)]), + positions_noise_map=al.ArrayIrregular([1]), + fluxes=al.ArrayIrregular([2]), + fluxes_noise_map=al.ArrayIrregular([3]), + time_delays=al.ArrayIrregular([4]), + time_delays_noise_map=al.ArrayIrregular([5]), + ) + + assert "name" in dataset.info + assert "positions : Grid2DIrregular" in dataset.info + assert "positions_noise_map : ArrayIrregular" in dataset.info + assert "fluxes : ArrayIrregular" in dataset.info + assert "fluxes_noise_map : ArrayIrregular" in dataset.info + assert "time_delays : ArrayIrregular" in dataset.info + assert "time_delays_noise_map : ArrayIrregular" in dataset.info diff --git a/test_autolens/point/triangles/conftest.py b/test_autolens/point/triangles/conftest.py index 1aec734ea..3c3620630 100644 --- a/test_autolens/point/triangles/conftest.py +++ b/test_autolens/point/triangles/conftest.py @@ -1,30 +1,30 @@ -import pytest -import autolens as al - - -@pytest.fixture -def grid(): - return al.Grid2D.uniform( - shape_native=(10, 10), - pixel_scales=1.0, - ) - - -@pytest.fixture -def tracer(): - isothermal_mass_profile = al.mp.Isothermal( - centre=(0.0, 0.0), - einstein_radius=1.6, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.9, angle=45.0), - ) - - lens_galaxy = al.Galaxy( - redshift=0.5, - mass=isothermal_mass_profile, - ) - - point_source = al.ps.Point(centre=(0.07, 0.07)) - - source_galaxy = al.Galaxy(redshift=1.0, point_0=point_source) - - return al.Tracer(galaxies=[lens_galaxy, source_galaxy]) +import pytest +import autolens as al + + +@pytest.fixture +def grid(): + return al.Grid2D.uniform( + shape_native=(10, 10), + pixel_scales=1.0, + ) + + +@pytest.fixture +def tracer(): + isothermal_mass_profile = al.mp.Isothermal( + centre=(0.0, 0.0), + einstein_radius=1.6, + ell_comps=al.convert.ell_comps_from(axis_ratio=0.9, angle=45.0), + ) + + lens_galaxy = al.Galaxy( + redshift=0.5, + mass=isothermal_mass_profile, + ) + + point_source = al.ps.Point(centre=(0.07, 0.07)) + + source_galaxy = al.Galaxy(redshift=1.0, point_0=point_source) + + return al.Tracer(galaxies=[lens_galaxy, source_galaxy]) diff --git a/test_autolens/point/triangles/test_pytree.py b/test_autolens/point/triangles/test_pytree.py index 6f3b30e31..aac41330b 100644 --- a/test_autolens/point/triangles/test_pytree.py +++ b/test_autolens/point/triangles/test_pytree.py @@ -1,12 +1,12 @@ -import autofit as af -from autogalaxy.profiles.mass import Isothermal - - -def test_isothermal_pytree(): - model = af.Model(Isothermal) - - children, aux = model.instance_flatten(Isothermal()) - instance = model.instance_unflatten(aux, children) - - assert isinstance(instance, Isothermal) - assert instance.centre == (0.0, 0.0) +import autofit as af +from autogalaxy.profiles.mass import Isothermal + + +def test_isothermal_pytree(): + model = af.Model(Isothermal) + + children, aux = model.instance_flatten(Isothermal()) + instance = model.instance_unflatten(aux, children) + + assert isinstance(instance, Isothermal) + assert instance.centre == (0.0, 0.0) diff --git a/test_autolens/point/triangles/test_regressions.py b/test_autolens/point/triangles/test_regressions.py index 295d662b7..f7a5785bc 100644 --- a/test_autolens/point/triangles/test_regressions.py +++ b/test_autolens/point/triangles/test_regressions.py @@ -1,76 +1,76 @@ -from autonerves.dictable import from_dict -import autolens as al -from autolens.point.solver import PointSolver - - -instance_dict = { - "type": "instance", - "class_path": "autofit.mapper.model.ModelInstance", - "arguments": { - "child_items": { - "type": "dict", - "arguments": { - "source_galaxy": { - "type": "instance", - "class_path": "autogalaxy.galaxy.galaxy.Galaxy", - "arguments": { - "redshift": 1.0, - "label": "cls6", - "light": { - "type": "instance", - "class_path": "autogalaxy.profiles.light.standard.exponential.Exponential", - "arguments": { - "effective_radius": 0.1, - "ell_comps": [0.4731722153284571, -0.27306667016189645], - "centre": [-0.04829335038475, 0.02350935356045], - "intensity": 0.1, - }, - }, - "point_0": { - "type": "instance", - "class_path": "autogalaxy.profiles.point_source.Point", - "arguments": { - "centre": [-0.04829335038475, 0.02350935356045] - }, - }, - }, - }, - "lens_galaxy": { - "type": "instance", - "class_path": "autogalaxy.galaxy.galaxy.Galaxy", - "arguments": { - "redshift": 0.5, - "label": "cls6", - "mass": { - "type": "instance", - "class_path": "autogalaxy.profiles.mass.total.isothermal.Isothermal", - "arguments": { - "ell_comps": [ - 0.05263157894736841, - 3.2227547345982974e-18, - ], - "einstein_radius": 1.6, - "centre": [0.0, 0.0], - }, - }, - }, - }, - }, - } - }, -} - - -def test_missing_multiple_image(grid): - instance = from_dict(instance_dict) - - tracer = al.Tracer(galaxies=[instance.lens_galaxy, instance.source_galaxy]) - - solver = PointSolver.for_grid( - grid=grid, - pixel_scale_precision=0.001, - ) - - triangle_positions = solver.solve( - tracer=tracer, source_plane_coordinate=instance.source_galaxy.point_0.centre - ) +from autonerves.dictable import from_dict +import autolens as al +from autolens.point.solver import PointSolver + + +instance_dict = { + "type": "instance", + "class_path": "autofit.mapper.model.ModelInstance", + "arguments": { + "child_items": { + "type": "dict", + "arguments": { + "source_galaxy": { + "type": "instance", + "class_path": "autogalaxy.galaxy.galaxy.Galaxy", + "arguments": { + "redshift": 1.0, + "label": "cls6", + "light": { + "type": "instance", + "class_path": "autogalaxy.profiles.light.standard.exponential.Exponential", + "arguments": { + "effective_radius": 0.1, + "ell_comps": [0.4731722153284571, -0.27306667016189645], + "centre": [-0.04829335038475, 0.02350935356045], + "intensity": 0.1, + }, + }, + "point_0": { + "type": "instance", + "class_path": "autogalaxy.profiles.point_source.Point", + "arguments": { + "centre": [-0.04829335038475, 0.02350935356045] + }, + }, + }, + }, + "lens_galaxy": { + "type": "instance", + "class_path": "autogalaxy.galaxy.galaxy.Galaxy", + "arguments": { + "redshift": 0.5, + "label": "cls6", + "mass": { + "type": "instance", + "class_path": "autogalaxy.profiles.mass.total.isothermal.Isothermal", + "arguments": { + "ell_comps": [ + 0.05263157894736841, + 3.2227547345982974e-18, + ], + "einstein_radius": 1.6, + "centre": [0.0, 0.0], + }, + }, + }, + }, + }, + } + }, +} + + +def test_missing_multiple_image(grid): + instance = from_dict(instance_dict) + + tracer = al.Tracer(galaxies=[instance.lens_galaxy, instance.source_galaxy]) + + solver = PointSolver.for_grid( + grid=grid, + pixel_scale_precision=0.001, + ) + + triangle_positions = solver.solve( + tracer=tracer, source_plane_coordinate=instance.source_galaxy.point_0.centre + ) diff --git a/test_autolens/test_config.py b/test_autolens/test_config.py index 172fade7d..0adc9a02d 100644 --- a/test_autolens/test_config.py +++ b/test_autolens/test_config.py @@ -1,37 +1,37 @@ -from pathlib import Path - -import pytest - -from autonerves import conf - -directory = Path(__file__).resolve().parent - - -class MockClass: - pass - - -@pytest.fixture(name="label_config") -def make_label_config(): - print(directory, "config") - - config = conf.Config( - directory / "config", output_path=directory / "output" - ) - - return config["notation"]["label"] - - -class TestLabel: - def test_basic(self, label_config): - assert label_config["label"]["centre_0"] == "y" - assert label_config["label"]["redshift"] == "z" - - def test_escaped(self, label_config): - assert label_config["label"]["gamma"] == r"\gamma" - print(label_config["label"]["contribution_factor"]) - assert label_config["label"]["contribution_factor"] == r"\omega_{\rm 0}" - - def test_exception(self, label_config): - with pytest.raises(KeyError): - label_config["subscript"].family(MockClass) +from pathlib import Path + +import pytest + +from autonerves import conf + +directory = Path(__file__).resolve().parent + + +class MockClass: + pass + + +@pytest.fixture(name="label_config") +def make_label_config(): + print(directory, "config") + + config = conf.Config( + directory / "config", output_path=directory / "output" + ) + + return config["notation"]["label"] + + +class TestLabel: + def test_basic(self, label_config): + assert label_config["label"]["centre_0"] == "y" + assert label_config["label"]["redshift"] == "z" + + def test_escaped(self, label_config): + assert label_config["label"]["gamma"] == r"\gamma" + print(label_config["label"]["contribution_factor"]) + assert label_config["label"]["contribution_factor"] == r"\omega_{\rm 0}" + + def test_exception(self, label_config): + with pytest.raises(KeyError): + label_config["subscript"].family(MockClass)